diff --git a/lib/error.ts b/lib/error.ts
deleted file mode 100644
index 398792e..0000000
--- a/lib/error.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export class NotFoundError extends Error {
- constructor(url: URL, context: string) {
- super(`Not found (${context}): ${url}`);
- this.name = "NotFoundError";
- }
-}
diff --git a/lib/feed-handler.test.ts b/lib/feed-handler.test.ts
index d9ae648..b63d263 100644
--- a/lib/feed-handler.test.ts
+++ b/lib/feed-handler.test.ts
@@ -1,8 +1,11 @@
import { strict as assert } from "node:assert";
import test from "node:test";
+import { json } from "itty-router";
import { feedHandler } from "./feed-handler.js";
+import { FetchWithErr, NotOk, OkResponse } from "./fetch.js";
import * as logger from "./logger.js";
-import { fetchMock, raiBaseUrl } from "./test/fetch-mock.js";
+import genresJson from "./test/generi.json";
+import feedJson from "./test/lastoriaingiallo.json";
import expectedJson from "./test/lastoriaingiallo.parsed.json" with {
type: "json",
};
@@ -16,14 +19,64 @@ test("feed-handler", async (t) => {
});
const baseUrl = new URL("https://test.dev/");
+const raiBaseUrl = new URL("https://rai.dev/");
+const mediaBaseUrl = new URL("https://media.dev/");
+
+const fetchMock: FetchWithErr = async (input, init) => {
+ const requestUrlStr = input.toString();
+ const url = new URL(requestUrlStr);
+ const { protocol, hostname, pathname, search } = url;
+
+ if (!(protocol === raiBaseUrl.protocol && hostname === raiBaseUrl.hostname)) {
+ throw new Error(`unexpected request to ${requestUrlStr}`);
+ }
+
+ if (pathname === "/generi.json") {
+ return json(genresJson) as OkResponse;
+ }
+
+ if (pathname === "/programmi/lastoriaingiallo.json") {
+ return json(feedJson) as OkResponse;
+ }
+ if (pathname === "/programmi/500.json") {
+ throw new NotOk(url, 500, "server error");
+ }
+ if (pathname === "/programmi/corrupt.json") {
+ return json({ foo: "bar" }) as OkResponse;
+ }
+
+ const relinkerRel = "/relinker/relinkerServlet.htm";
+ const relinkerSearchStart = "?cont=";
+ if (
+ init?.method === "HEAD" &&
+ pathname === relinkerRel &&
+ search.startsWith(relinkerSearchStart)
+ ) {
+ const urlStart = requestUrlStr.replace(
+ new URL(`${relinkerRel}${relinkerSearchStart}`, raiBaseUrl).toString(),
+ mediaBaseUrl.toString(),
+ );
+ const url = `${urlStart}.mp3`;
+ return {
+ url: url,
+ headers: new Headers({
+ "Content-Type": "audio/mpeg",
+ "Content-Length": "123456789",
+ }),
+ } as OkResponse;
+ }
+
+ throw new NotOk(url, 404, "not found");
+};
const conf = {
baseUrl,
raiBaseUrl,
poolSize: 1,
- fetch: fetchMock,
+ fetchWithErr: fetchMock,
logger: logger.disabled,
};
+
async function rssFeedSuccess() {
const req = new Request("https://test.dev/programmi/lastoriaingiallo.xml");
const resp = await feedHandler(conf, req);
@@ -42,7 +95,7 @@ async function rssFeedFail404() {
const text = await resp.text();
assert.strictEqual(
text,
- "404
Not Found",
+ "404
not found",
);
}
diff --git a/lib/feed-handler.ts b/lib/feed-handler.ts
index e67c40b..b1050e9 100644
--- a/lib/feed-handler.ts
+++ b/lib/feed-handler.ts
@@ -1,13 +1,13 @@
import { createResponse } from "itty-router";
-import { NotFoundError } from "./error.js";
import { convertFeed } from "./feed.js";
+import { FetchWithErr, NotOk } from "./fetch.js";
import { Logger } from "./logger.js";
type Config = {
baseUrl: URL;
raiBaseUrl: URL;
poolSize: number;
- fetch: typeof fetch;
+ fetchWithErr: FetchWithErr;
logger: Logger;
};
export async function feedHandler(
@@ -22,14 +22,14 @@ export async function feedHandler(
try {
feedXml = await convertFeed(conf, jsonPath);
} catch (e) {
- const headers = new Headers({ "Content-Type": "application/xml" });
+ conf.logger.error("error converting feed", jsonPath, e);
+ const contentType = "application/xml";
+ const headers = new Headers({ "Content-Type": contentType });
let status = 500;
let body = "500
server error";
- if (e instanceof NotFoundError) {
- status = 404;
- body = "404
Not Found";
- } else {
- conf.logger.error("error converting feed", jsonPath, e);
+ if (e instanceof NotOk && e.status === 404) {
+ status = e.status;
+ body = "404
not found";
}
return new Response(body, { status, headers });
}
diff --git a/lib/feed.test.ts b/lib/feed.test.ts
index 7639ab8..5d79d5e 100644
--- a/lib/feed.test.ts
+++ b/lib/feed.test.ts
@@ -1,8 +1,8 @@
import { strict as assert } from "node:assert";
import test from "node:test";
-import { error, json } from "itty-router";
-import { NotFoundError } from "./error.js";
+import { json } from "itty-router";
import { ConvertConf, convertFeed } from "./feed.js";
+import { FetchWithErr, NotOk, OkResponse } from "./fetch.js";
import feedJson from "./test/lastoriaingiallo.json" with { type: "json" };
import expectedJson from "./test/lastoriaingiallo.parsed.json" with {
type: "json",
@@ -18,12 +18,11 @@ test("feed", async (t) => {
const baseUrl = new URL("https://test.dev/");
const raiBaseUrl = new URL("https://rai.dev/");
const poolSize = 5; // arbitrary
-const feedFetchFn: typeof fetch = async (input) => {
- assert.strictEqual(input.toString(), "https://rai.dev/programmi/foo.json");
- return json(feedJson);
-};
-const mediaFetchFn: typeof fetch = async (input) =>
- ({
+
+const feedFetchFn: FetchWithErr = () =>
+ Promise.resolve(json(feedJson) as OkResponse);
+const mediaFetchFn: FetchWithErr = (input) =>
+ Promise.resolve({
url: input
.toString()
.replace(/.+cont=(.*)/, (_, cont) => `https://media.dev/${cont}.mp3`),
@@ -32,33 +31,32 @@ const mediaFetchFn: typeof fetch = async (input) =>
"content-type": "audio/mpeg",
"content-length": "123456789",
}),
- }) as Response;
+ } as OkResponse);
async function convertFeedSuccess() {
- const fetchFn: typeof fetch = async (input) => {
+ const fetchWithErr: FetchWithErr = async (input) => {
return input.toString().endsWith("foo.json")
? feedFetchFn(input)
: mediaFetchFn(input);
};
- const conf: ConvertConf = { raiBaseUrl, baseUrl, poolSize, fetch: fetchFn };
+ const conf: ConvertConf = { raiBaseUrl, baseUrl, poolSize, fetchWithErr };
const feed = await convertFeed(conf, "programmi/foo.json");
const parsed = parseFeed(feed);
assert.deepStrictEqual(parsed, expectedJson);
}
async function convertFeed404() {
- const fetchFn = async () => error(404, "Not found");
- const conf: ConvertConf = { raiBaseUrl, baseUrl, poolSize, fetch: fetchFn };
- const expectedErr = new NotFoundError(
- new URL("https://rai.dev/programmi/foo.json"),
- "fetching feed",
- );
- await assert.rejects(convertFeed(conf, "programmi/foo.json"), expectedErr);
+ const url = new URL("https://rai.dev/programmi/foo.json");
+ const notFound = new NotOk(url, 404, "Not Found");
+ const fetchWithErr = () => Promise.reject(notFound);
+ const conf: ConvertConf = { raiBaseUrl, baseUrl, poolSize, fetchWithErr };
+ await assert.rejects(convertFeed(conf, "programmi/foo.json"), notFound);
}
async function convertFeedNonCompliantJson() {
- const fetchFn = async () => json({ foo: "bar" });
- const conf: ConvertConf = { raiBaseUrl, baseUrl, poolSize, fetch: fetchFn };
+ const fetchWithErr = () =>
+ Promise.resolve(json({ foo: "bar" }) as OkResponse);
+ const conf: ConvertConf = { raiBaseUrl, baseUrl, poolSize, fetchWithErr };
const expectedErr = /^Error: failed to parse feed JSON/;
await assert.rejects(convertFeed(conf, "programmi/foo.json"), expectedErr);
}
diff --git a/lib/feed.ts b/lib/feed.ts
index 6659558..36896da 100644
--- a/lib/feed.ts
+++ b/lib/feed.ts
@@ -1,8 +1,8 @@
import { PromisePool } from "@supercharge/promise-pool";
import { z } from "zod";
import { Podcast } from "../build/podcast/index.js";
-import { NotFoundError } from "./error.js";
-import { fetchInfo } from "./media.js";
+import { FetchWithErr } from "./fetch.js";
+import * as media from "./media.js";
const cardSchema = z.object({
episode_title: z.string(),
@@ -32,58 +32,39 @@ export type ConvertConf = {
raiBaseUrl: URL;
baseUrl: URL;
poolSize: number;
- fetch: typeof fetch;
+ fetchWithErr: FetchWithErr;
};
export async function convertFeed(
c: ConvertConf,
relUrl: string,
): Promise {
+ const fetchInfo = media.mkFetchInfo(c.fetchWithErr);
const convertor = new Convertor({
raiBaseUrl: c.raiBaseUrl,
poolSize: c.poolSize,
- fetch: c.fetch,
+ fetchInfo,
});
- const feedJson = await fetchFeed(c, relUrl);
- return convertor.convert(feedJson);
-}
-
-type FetcherConf = {
- raiBaseUrl: URL;
- fetch: typeof fetch;
-};
-
-async function fetchFeed(
- { raiBaseUrl, fetch }: FetcherConf,
- relUrl: string,
-): Promise {
- const url = new URL(relUrl, raiBaseUrl);
- const res = await fetch(url);
- if (!res.ok) {
- if (res.status === 404) {
- throw new NotFoundError(url, "fetching feed");
- }
- throw new Error(
- `Failed to fetch ${url}: ${res.status} ${res.statusText}`.trim(),
- );
- }
- return res.json();
+ const url = new URL(relUrl, c.raiBaseUrl);
+ const resp = await c.fetchWithErr(url);
+ const json = await resp.json();
+ return convertor.convert(json);
}
type ConvertorConf = {
raiBaseUrl: URL;
poolSize: number;
- fetch: typeof fetch;
+ fetchInfo: media.FetchInfo;
};
class Convertor {
readonly #raiBaseUrl: URL;
readonly #poolSize: number;
- readonly #fetch: typeof fetch;
+ readonly #fetchInfo: media.FetchInfo;
- constructor({ raiBaseUrl, poolSize, fetch }: ConvertorConf) {
+ constructor({ raiBaseUrl, poolSize, fetchInfo }: ConvertorConf) {
this.#raiBaseUrl = raiBaseUrl;
this.#poolSize = poolSize;
- this.#fetch = fetch;
+ this.#fetchInfo = fetchInfo;
}
// TODO: feedUrl, siteUrl
@@ -125,7 +106,7 @@ class Convertor {
async convertCard(card: Card) {
const imageUrl = new URL(card.image, this.#raiBaseUrl).toString();
const date = new Date(card.track_info.date);
- const mediaInfo = await fetchInfo(this.#fetch, card.downloadable_audio.url);
+ const mediaInfo = await this.#fetchInfo(card.downloadable_audio.url);
const url = mediaInfo.url.toString();
return {
title: card.episode_title,
diff --git a/lib/fetch.ts b/lib/fetch.ts
new file mode 100644
index 0000000..a61cb99
--- /dev/null
+++ b/lib/fetch.ts
@@ -0,0 +1,48 @@
+export { FetchWithErr, NotOk, OkResponse, mkFetchWithErr };
+
+type FetchWithErr = (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+) => Promise;
+
+type OkResponse = Response & { ok: true };
+
+/**
+ * Returns a fetch function that errors on a non-ok response
+ * @param fetchFn - The underlying fetch function to use
+ * @returns A fetch function that throws an error for non-ok responses
+ */
+const mkFetchWithErr =
+ (fetchFn: typeof fetch): FetchWithErr =>
+ async (input, init) => {
+ const res = await fetchFn(input, init);
+ if (!res.ok) {
+ // maybe we should use the url from the response?
+ const url = new URL(input.toString());
+ throw new NotOk(url, res.status, res.statusText);
+ }
+ // this assertion really shouldn't be necessary...
+ return res as OkResponse;
+ };
+
+/**
+ * Error class for non-ok responses
+ */
+class NotOk extends Error {
+ readonly url: URL;
+ readonly status: number;
+ readonly statusText: string;
+
+ /**
+ * @param url - The URL that resulted in a non-ok response
+ * @param status - The status code of the non-ok response
+ * @param statusText - The status text of the non-ok response
+ */
+ constructor(url: URL, status: number, statusText: string) {
+ super(`Not ok: ${status} ${statusText} (${url})`);
+ this.name = "NotOkError";
+ this.url = url;
+ this.status = status;
+ this.statusText = statusText;
+ }
+}
diff --git a/lib/genres.test.ts b/lib/genres.test.ts
index b1864fd..a069dae 100644
--- a/lib/genres.test.ts
+++ b/lib/genres.test.ts
@@ -1,7 +1,7 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import { json } from "itty-router";
-import { NotFoundError } from "./error.js";
+import { FetchWithErr, NotOk, OkResponse } from "./fetch.js";
import { genresHtml } from "./genres.js";
import genresJson from "./test/generi.json" with { type: "json" };
@@ -12,47 +12,36 @@ test("genres", async (t) => {
});
async function genresHtmlSuccess() {
- const fetchFn: typeof fetch = async (input) => {
- assert.strictEqual(input.toString(), "https://rai.dev/generi.json");
- return json(genresJson);
- };
- const conf = confWithFetch(fetchFn);
+ const fetchWithErr: FetchWithErr = () =>
+ Promise.resolve(json(genresJson) as OkResponse);
+ const conf = confWithFetch(fetchWithErr);
await genresHtml(conf);
}
async function genresHtmlNotFound() {
- const fetchFn: typeof fetch = async (input) => {
- assert.strictEqual(input.toString(), "https://rai.dev/generi.json");
- return new Response("Not found", { status: 404 });
- };
- const conf = confWithFetch(fetchFn);
-
- const expectedErr = new NotFoundError(
- new URL("https://rai.dev/generi.json"),
- "fetching genres",
- );
+ const url = new URL("https://rai.dev/generi.json");
+ const notFound = new NotOk(url, 404, "Not Found");
+ const fetchWithErr: FetchWithErr = () => Promise.reject(notFound);
+ const conf = confWithFetch(fetchWithErr);
+
const p = genresHtml(conf);
- await assert.rejects(p, expectedErr);
+ await assert.rejects(p, notFound);
}
async function genresHtml500() {
- const fetchFn: typeof fetch = async (input) => {
- assert.strictEqual(input.toString(), "https://rai.dev/generi.json");
- return new Response("Internal Server Error", { status: 500 });
- };
- const conf = confWithFetch(fetchFn);
-
- const expectedErr = new Error(
- "Failed to fetch https://rai.dev/generi.json 500",
- );
+ const url = new URL("https://rai.dev/generi.json");
+ const internalServerErr = new NotOk(url, 500, "Internal Server Error");
+ const fetchWithErr: FetchWithErr = () => Promise.reject(internalServerErr);
+ const conf = confWithFetch(fetchWithErr);
+
const p = genresHtml(conf);
- await assert.rejects(p, expectedErr);
+ await assert.rejects(p, internalServerErr);
}
-const confWithFetch = (fetchFn: typeof fetch) => ({
+const confWithFetch = (fetchWithErr: FetchWithErr) => ({
baseUrl: new URL("https://test.dev/"),
raiBaseUrl: new URL("https://rai.dev/"),
genresRel: "generi.json",
- fetch: fetchFn,
+ fetchWithErr,
});
diff --git a/lib/genres.ts b/lib/genres.ts
index 3537963..ca03045 100644
--- a/lib/genres.ts
+++ b/lib/genres.ts
@@ -1,5 +1,7 @@
import { z } from "zod";
-import { NotFoundError } from "./error.js";
+import { FetchWithErr } from "./fetch.js";
+
+export { Conf, genresHtml };
const cardSchema = z.object({
title: z.string(),
@@ -13,30 +15,20 @@ const schema = z.object({
}),
});
-export type Conf = {
+type Conf = {
raiBaseUrl: URL;
baseUrl: URL;
- fetch: typeof fetch;
+ fetchWithErr: FetchWithErr;
};
-export async function genresHtml(c: Conf): Promise {
+async function genresHtml(c: Conf): Promise {
const json = await fetchGenres(c);
return renderGenres(c.baseUrl, json);
}
const fetchGenres = async (c: Conf) => {
const url = new URL("generi.json", c.raiBaseUrl);
- const res = await c.fetch(url.toString());
-
- if (!res.ok) {
- if (res.status === 404) {
- throw new NotFoundError(url, "fetching genres");
- }
- throw new Error(
- `Failed to fetch ${url} ${res.status} ${res.statusText}`.trim(),
- );
- }
-
+ const res = await c.fetchWithErr(url.toString());
return res.json();
};
diff --git a/lib/handler.test.ts b/lib/handler.test.ts
index d42df3d..f14042c 100644
--- a/lib/handler.test.ts
+++ b/lib/handler.test.ts
@@ -1,16 +1,18 @@
import { strict as assert } from "node:assert";
import test from "node:test";
+import { error, json } from "itty-router";
import { mkFetchHandler } from "./handler.js";
import * as logger from "./logger.js";
-import { fetchMock } from "./test/fetch-mock.js";
+import genresJson from "./test/generi.json";
+import feedJson from "./test/lastoriaingiallo.json";
import expectedJson from "./test/lastoriaingiallo.parsed.json" with {
type: "json",
};
import { parseFeed } from "./test/parse-feed.js";
test("handler", async (t) => {
- await t.test(indexSucccess);
- await t.test(rssFeedSuccess);
+ await t.test(indexSuccess);
+ await test(rssFeedSuccess);
await t.test(rssFeedFail404);
await t.test(rssFeedFail500);
await t.test(rssFeedFailCorrupt);
@@ -19,16 +21,9 @@ test("handler", async (t) => {
const baseUrl = new URL("https://test.dev/");
const raiBaseUrl = new URL("https://rai.dev/");
+const mediaBaseUrl = new URL("https://media.dev/");
-const fetchHandler = mkFetchHandler({
- baseUrl,
- raiBaseUrl,
- poolSize: 1,
- fetch: fetchMock,
- logger: logger.disabled,
-});
-
-async function indexSucccess() {
+async function indexSuccess() {
const req = new Request("arbitrary://arbitrary/");
const resp = await fetchHandler(req);
@@ -58,7 +53,7 @@ async function rssFeedFail404() {
const text = await resp.text();
assert.strictEqual(
text,
- "404
Not Found",
+ "404
not found",
);
}
@@ -95,3 +90,58 @@ async function notFound() {
const text = await resp.text();
assert.strictEqual(text, "Not found.");
}
+
+const fetchMock: typeof fetch = async (input, init) => {
+ const requestUrlStr = input.toString();
+ const { protocol, hostname, pathname, search } = new URL(requestUrlStr);
+
+ if (!(protocol === raiBaseUrl.protocol && hostname === raiBaseUrl.hostname)) {
+ throw new Error(`unexpected request to ${requestUrlStr}`);
+ }
+
+ if (pathname === "/generi.json") {
+ return json(genresJson);
+ }
+
+ if (pathname === "/programmi/lastoriaingiallo.json") {
+ return json(feedJson);
+ }
+ if (pathname === "/programmi/500.json") {
+ return error(500, "internal server error");
+ }
+ if (pathname === "/programmi/corrupt.json") {
+ return json({ foo: "bar" });
+ }
+
+ const relinkerRel = "/relinker/relinkerServlet.htm";
+ const relinkerSearchStart = "?cont=";
+ if (
+ init?.method === "HEAD" &&
+ pathname === relinkerRel &&
+ search.startsWith(relinkerSearchStart)
+ ) {
+ const urlStart = requestUrlStr.replace(
+ new URL(`${relinkerRel}${relinkerSearchStart}`, raiBaseUrl).toString(),
+ mediaBaseUrl.toString(),
+ );
+ const url = `${urlStart}.mp3`;
+ return {
+ url: url,
+ headers: new Headers({
+ "Content-Type": "audio/mpeg",
+ "Content-Length": "123456789",
+ }),
+ ok: true,
+ } as Response;
+ }
+
+ return error(404, "not found");
+};
+
+const fetchHandler = mkFetchHandler({
+ baseUrl,
+ raiBaseUrl,
+ poolSize: 1,
+ fetch: fetchMock,
+ logger: logger.disabled,
+});
diff --git a/lib/handler.ts b/lib/handler.ts
index 8e356c1..f5df3c2 100644
--- a/lib/handler.ts
+++ b/lib/handler.ts
@@ -1,5 +1,6 @@
import { Router, createResponse, error, html, text } from "itty-router";
import { feedHandler } from "./feed-handler.js";
+import { mkFetchWithErr } from "./fetch.js";
import { genresHtml } from "./genres.js";
import { Logger } from "./logger.js";
@@ -13,11 +14,27 @@ export type FetchHandlerConfig = {
type FetchHandler = (req: Request) => Promise;
export function mkFetchHandler(conf: FetchHandlerConfig): FetchHandler {
+ const fetchWithErr = mkFetchWithErr(conf.fetch);
+
+ const fetchGenresConf = {
+ baseUrl: conf.baseUrl,
+ raiBaseUrl: conf.raiBaseUrl,
+ fetchWithErr,
+ logger: conf.logger,
+ };
const fetchGenres = async () => {
- const gh = await genresHtml(conf);
+ const gh = await genresHtml(fetchGenresConf);
return html(gh, { headers: { "Content-Language": "it" } });
};
- const fetchFeed = (request: Request) => feedHandler(conf, request);
+
+ const fetchFeedConf = {
+ baseUrl: conf.baseUrl,
+ raiBaseUrl: conf.raiBaseUrl,
+ poolSize: conf.poolSize,
+ fetchWithErr,
+ logger: conf.logger,
+ };
+ const fetchFeed = (request: Request) => feedHandler(fetchFeedConf, request);
const router = Router()
.get("/", fetchGenres)
diff --git a/lib/media.test.ts b/lib/media.test.ts
index dc4ff5b..5fa2bee 100644
--- a/lib/media.test.ts
+++ b/lib/media.test.ts
@@ -1,6 +1,7 @@
import { strict as assert } from "node:assert";
import test from "node:test";
-import { fetchInfo } from "./media.js";
+import { FetchWithErr, OkResponse } from "./fetch.js";
+import { mkFetchInfo } from "./media.js";
test("media", (t) => {
return t.test(fetchInfoSuccess);
@@ -10,7 +11,7 @@ async function fetchInfoSuccess() {
const url =
"https://mediapolisvod.rai.it/relinker/relinkerServlet.htm?cont=PE3wc6etKfssSlashNKfaoXssSlashpWcgeeqqEEqualeeqqEEqual";
const mediaUrl = new URL("https://test.dev/foo.mp3");
- const fetch: typeof globalThis.fetch = async () =>
+ const fetch: FetchWithErr = async () =>
({
url: mediaUrl.toString(),
status: 200,
@@ -18,8 +19,10 @@ async function fetchInfoSuccess() {
"content-type": "audio/mpeg",
"content-length": "123456789",
}),
- }) as Response;
- const info = await fetchInfo(fetch, url);
+ }) as OkResponse;
+ const fetchInfo = mkFetchInfo(fetch);
+
+ const info = await fetchInfo(url);
assert.deepStrictEqual(info, {
url: mediaUrl,
size: 123456789,
diff --git a/lib/media.ts b/lib/media.ts
index 33d396e..a6eea41 100644
--- a/lib/media.ts
+++ b/lib/media.ts
@@ -1,7 +1,8 @@
-export { MediaInfo, MediaUrl, fetchInfo };
+import { FetchWithErr } from "./fetch.js";
-const chromeAgent =
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/[latest_version] Safari/537.36";
+export { FetchInfo, MediaInfo, MediaUrl, mkFetchInfo };
+
+type FetchInfo = (url: string) => Promise;
type MediaUrl = URL;
@@ -11,40 +12,42 @@ type MediaInfo = {
};
const relinkerRe = /^\?cont=[a-zA-Z0-9]+$/;
-async function fetchInfo(
- fetch: typeof globalThis.fetch,
- url: string,
-): Promise {
- const mediaUrl = mkMediaUrl(url);
- if (typeof mediaUrl === "string") {
- const err = `Invalid URL (${url}): ${mediaUrl}`;
- throw new Error(err);
- }
- const chromeHeadInit: RequestInit = {
- method: "HEAD",
- headers: {
- "User-Agent": chromeAgent,
- },
- };
- const resp = await fetch(url, chromeHeadInit);
+const mkFetchInfo =
+ (fetchWithErr: FetchWithErr): FetchInfo =>
+ async (url) => {
+ const mediaUrl = mkMediaUrl(url);
+ if (typeof mediaUrl === "string") {
+ const err = `Invalid URL (${url}): ${mediaUrl}`;
+ throw new Error(err);
+ }
- const expectedContentType = "audio/mpeg";
- const contentType = resp.headers.get("content-type");
- if (contentType !== expectedContentType) {
- throw new Error(
- `Invalid content type: ${contentType}, wanted ${expectedContentType}`,
- );
- }
+ const chromeAgent =
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/[latest_version] Safari/537.36";
+ const chromeHeadInit: RequestInit = {
+ method: "HEAD",
+ headers: {
+ "User-Agent": chromeAgent,
+ },
+ };
+ const resp = await fetchWithErr(url, chromeHeadInit);
- const contentLength = resp.headers.get("content-length");
- const length = Number(contentLength);
- if (Number.isNaN(length)) {
- throw new Error(`Invalid content length: ${contentLength}`);
- }
+ const expectedContentType = "audio/mpeg";
+ const contentType = resp.headers.get("content-type");
+ if (contentType !== expectedContentType) {
+ throw new Error(
+ `Invalid content type: ${contentType}, wanted ${expectedContentType}`,
+ );
+ }
- return { url: new URL(resp.url), size: length };
-}
+ const contentLength = resp.headers.get("content-length");
+ const length = Number(contentLength);
+ if (Number.isNaN(length)) {
+ throw new Error(`Invalid content length: ${contentLength}`);
+ }
+
+ return { url: new URL(resp.url), size: length };
+ };
function mkMediaUrl(urlStr: string): MediaUrl | string {
let url: URL;
diff --git a/lib/test/fetch-mock.ts b/lib/test/fetch-mock.ts
deleted file mode 100644
index a74c33a..0000000
--- a/lib/test/fetch-mock.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { error, json } from "itty-router";
-import genresJson from "./generi.json";
-import feedJson from "./lastoriaingiallo.json";
-
-export const raiBaseUrl = new URL("https://rai.dev/");
-const mediaBaseUrl = new URL("https://media.dev/");
-export const fetchMock: typeof fetch = async (input, init) => {
- const requestUrlStr = input.toString();
- const { protocol, hostname, pathname, search } = new URL(requestUrlStr);
-
- if (!(protocol === raiBaseUrl.protocol && hostname === raiBaseUrl.hostname)) {
- throw new Error(`unexpected request to ${requestUrlStr}`);
- }
-
- if (pathname === "/generi.json") {
- return json(genresJson);
- }
-
- if (pathname === "/programmi/lastoriaingiallo.json") {
- return json(feedJson);
- }
- if (pathname === "/programmi/500.json") {
- return error(500, "internal server error");
- }
- if (pathname === "/programmi/corrupt.json") {
- return json({ foo: "bar" });
- }
-
- const relinkerRel = "/relinker/relinkerServlet.htm";
- const relinkerSearchStart = "?cont=";
- if (
- init?.method === "HEAD" &&
- pathname === relinkerRel &&
- search.startsWith(relinkerSearchStart)
- ) {
- const urlStart = requestUrlStr.replace(
- new URL(`${relinkerRel}${relinkerSearchStart}`, raiBaseUrl).toString(),
- mediaBaseUrl.toString(),
- );
- const url = `${urlStart}.mp3`;
- return {
- url: url,
- headers: new Headers({
- "Content-Type": "audio/mpeg",
- "Content-Length": "123456789",
- }),
- } as Response;
- }
-
- return error(404, "not found");
-};