diff --git a/lib/feed.ts b/lib/feed.ts index 5f0ecc4..6659558 100644 --- a/lib/feed.ts +++ b/lib/feed.ts @@ -2,7 +2,7 @@ import { PromisePool } from "@supercharge/promise-pool"; import { z } from "zod"; import { Podcast } from "../build/podcast/index.js"; import { NotFoundError } from "./error.js"; -import { Fetcher as MediaFetcher } from "./media.js"; +import { fetchInfo } from "./media.js"; const cardSchema = z.object({ episode_title: z.string(), @@ -38,11 +38,10 @@ export async function convertFeed( c: ConvertConf, relUrl: string, ): Promise { - const mediaFetcher = new MediaFetcher(c.fetch); const convertor = new Convertor({ raiBaseUrl: c.raiBaseUrl, poolSize: c.poolSize, - fetcher: mediaFetcher, + fetch: c.fetch, }); const feedJson = await fetchFeed(c, relUrl); return convertor.convert(feedJson); @@ -73,18 +72,18 @@ async function fetchFeed( type ConvertorConf = { raiBaseUrl: URL; poolSize: number; - fetcher: MediaFetcher; + fetch: typeof fetch; }; class Convertor { readonly #raiBaseUrl: URL; readonly #poolSize: number; - readonly #fetcher: MediaFetcher; + readonly #fetch: typeof fetch; - constructor({ raiBaseUrl, poolSize, fetcher }: ConvertorConf) { + constructor({ raiBaseUrl, poolSize, fetch }: ConvertorConf) { this.#raiBaseUrl = raiBaseUrl; this.#poolSize = poolSize; - this.#fetcher = fetcher; + this.#fetch = fetch; } // TODO: feedUrl, siteUrl @@ -126,9 +125,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 this.#fetcher.fetchInfo( - card.downloadable_audio.url, - ); + const mediaInfo = await fetchInfo(this.#fetch, card.downloadable_audio.url); const url = mediaInfo.url.toString(); return { title: card.episode_title, diff --git a/lib/media.test.ts b/lib/media.test.ts index 3254c32..dc4ff5b 100644 --- a/lib/media.test.ts +++ b/lib/media.test.ts @@ -1,7 +1,6 @@ import { strict as assert } from "node:assert"; -import test, { before } from "node:test"; -import * as logger from "./logger.js"; -import { Fetcher } from "./media.js"; +import test from "node:test"; +import { fetchInfo } from "./media.js"; test("media", (t) => { return t.test(fetchInfoSuccess); @@ -11,7 +10,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 f: typeof fetch = async () => + const fetch: typeof globalThis.fetch = async () => ({ url: mediaUrl.toString(), status: 200, @@ -20,8 +19,7 @@ async function fetchInfoSuccess() { "content-length": "123456789", }), }) as Response; - const fetcher = new Fetcher(f); - const info = await fetcher.fetchInfo(url); + const info = await fetchInfo(fetch, url); assert.deepStrictEqual(info, { url: mediaUrl, size: 123456789, diff --git a/lib/media.ts b/lib/media.ts index faad63c..33d396e 100644 --- a/lib/media.ts +++ b/lib/media.ts @@ -1,51 +1,49 @@ -export type MediaUrl = URL; +export { MediaInfo, MediaUrl, fetchInfo }; -export type MediaInfo = { +const chromeAgent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/[latest_version] Safari/537.36"; + +type MediaUrl = URL; + +type MediaInfo = { url: URL; size: number; }; -const chromeAgent = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/[latest_version] Safari/537.36"; const relinkerRe = /^\?cont=[a-zA-Z0-9]+$/; -const expectedContentType = "audio/mpeg"; -export class Fetcher { - readonly #fetch: typeof fetch; - - constructor(f: typeof fetch = fetch.bind(globalThis)) { - this.#fetch = f; +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); } - async fetchInfo(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 this.#fetch(url, chromeHeadInit); - - const contentType = resp.headers.get("content-type"); - if (contentType !== expectedContentType) { - throw new Error( - `Invalid content type: ${contentType}, wanted ${expectedContentType}`, - ); - } + const chromeHeadInit: RequestInit = { + method: "HEAD", + headers: { + "User-Agent": chromeAgent, + }, + }; + const resp = await fetch(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 {