npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tilewise/source-arr

v0.1.2

Published

**The \*arr apps as resources: Radarr's movies, Sonarr's series, and each one's download queue, through their API v3.**

Readme

🎬 @tilewise/source-arr

The *arr apps as resources: Radarr's movies, Sonarr's series, and each one's download queue, through their API v3.

Radarr Sonarr openapi-fetch types

Four resources, each a definition and a source for @tilewise/source's serve. Every call is typed by the app's own OpenAPI description: its params, its body and its answer.

| Resource | From | Served | | ----------------- | -------------- | ------------------------------------ | | 🎞️ movies | radarrMovies | whole, in memory: every operator | | 📺 series | sonarrSeries | whole, in memory: every operator | | 📥 radarr-queue | radarrQueue | paged, filtered and sorted by Radarr | | 📥 sonarr-queue | sonarrQueue | paged, filtered and sorted by Sonarr |

🔄 Where each query goes

flowchart LR
  lib["POST /movies/query<br/>POST /series/query"] --> whole[("GET /api/v3/movie<br/>GET /api/v3/series")]
  whole -->|every row| memory["tilewise, in memory<br/>filter · sort · group · page"]
  queue["POST /radarr-queue/query"] --> params["the app's params<br/>movieIds · status · protocol"]
  params --> paged[("GET /api/v3/queue")]

  classDef route fill:#6366F1,stroke:#4338CA,color:#fff
  classDef tilewise fill:#0F766E,stroke:#115E59,color:#fff
  classDef app fill:#F59E0B,stroke:#B45309,color:#111
  class lib,queue route
  class memory,params tilewise
  class whole,paged app

🚀 Quick start

import { router, serve } from '@tilewise/source';
import { radarrMovies, radarrQueue, sonarrQueue, sonarrSeries } from '@tilewise/source-arr';

const radarr = {
  url: 'http://localhost:7878',
  apiKey: process.env.RADARR_KEY,
};

const sonarr = {
  url: 'http://localhost:8989',
  apiKey: process.env.SONARR_KEY,
};

const resources = [
  radarrMovies({ ...radarr, add: { rootFolderPath: '/movies' } }),
  radarrQueue({ ...radarr, remove: { blocklist: true } }),
  sonarrSeries({ ...sonarr, add: { rootFolderPath: '/tv' } }),
  sonarrQueue(sonarr),
];

const route = router(resources.map(({ definition, source }) => serve(definition, source)));

[!TIP] The API key is in each app's Settings → General → Security. Behind a proxy, url takes the app's path too: https://home.example/radarr.

🎞️ Movies and series

The apps answer their whole library at once, so tilewise serves its queries in memory: every operator, sort, group, search and page the kinds allow.

// Unwatched dramas, newest first
await movies.query({
  filter: {
    all: [
      { field: 'genres', op: 'in', value: ['Drama'] },
      { field: 'hasFile', op: 'equals', value: false },
    ],
  },
  sort: [{ field: 'year', dir: 'desc' }],
});

// Add a movie where `add` says, searching for it if told
await movies.create({
  values: { tmdbId: 603, title: 'The Matrix', qualityProfileId: 4 },
});

// Stop monitoring it
await movies.update(42, { values: { monitored: false } });

A change reads the row and sends it back whole, as the apps' PUT takes it, its files left where they are. A create needs add; without it the resource creates nothing. A delete removes the files only when remove says so.

| Option | Radarr | Sonarr | | ---------- | --------------------------------------------- | --------------------------------------------------------------------------------- | | add | rootFolderPath, searchForMovie, monitor | rootFolderPath, monitor, searchForMissingEpisodes, languageProfileId (v3) | | remove | deleteFiles, addImportExclusion | deleteFiles, addImportListExclusion | | resource | 'movies' | 'series' |

| Writes | Radarr | Sonarr | | ------ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | create | tmdbId, title, qualityProfileId; monitored, minimumAvailability, year, tags | tvdbId, title, qualityProfileId; seriesType, monitored, seasonFolder, tags | | update | monitored, qualityProfileId, minimumAvailability, tags | monitored, qualityProfileId, seriesType, seasonFolder, tags |

📥 Queues

A queue is paged, filtered and sorted by the app, as its queue controller does it, so the resource offers only what the controller says exactly:

| Field | Filters | Sorts | | ------------------ | ----------------------------------------- | ----- | | movie / series | is, in | | | status | is, is not, in, none of, empty, not empty | | | protocol | is (one protocol) | | | size | | ✅ |

All of a filter's conditions become the values each param may hold in common, so this query:

await queue.query({
  filter: {
    all: [
      { field: 'status', op: 'in', value: ['downloading', 'queued'] },
      { field: 'status', op: 'notIn', value: ['downloading'] },
      { field: 'movie', op: 'in', value: [1, 2] },
    ],
  },
  sort: [{ field: 'size', dir: 'desc' }],
  page: { take: 20 },
});

…asks Radarr for one status, queued:

GET /api/v3/queue?page=1&pageSize=20&sortKey=size&sortDirection=descending
  &includeUnknownMovieItems=true&movieIds=1&movieIds=2&status=queued
X-Api-Key: …

A filter no item can meet asks the app nothing. A queue joins by neither any nor not, and sorts by size alone: the app orders text by its culture, and breaks ties by progress. Sonarr's queue has each item's episode and season too.

[!NOTE] Past the last page the apps answer the last page again; the resource answers none there, as the contract says.

Removing a download takes the app's own choices (remove):

| Choice | Default | What | | ------------------ | ------- | -------------------------------------------------------- | | removeFromClient | true | it's removed from the download client too | | blocklist | false | its release is blocklisted, so it isn't grabbed again | | skipRedownload | false | no other release is searched for after a blocklist | | changeCategory | false | its category in the client changes, in place of removing |

🔧 Regenerating the types

src/generated is written by openapi-typescript from the OpenAPI descriptions on Radarr's and Sonarr's develop branches. ESLint and Prettier skip it, and it isn't edited by hand.

pnpm --filter @tilewise/source-arr generate

🗂️ Layout

src/
├── index.ts              the package's exports
├── types/                the queue's types, one concept to a file
│   ├── queue-item.ts     a queue item and page
│   ├── queue-request.ts  a query's part, and the queue call's params
│   ├── queue-removal.ts  what a download is removed with
│   └── queue-app.ts      what each app gives the shared queue
├── client/
│   └── client.ts         an openapi-fetch client per app
├── generated/            pnpm generate; never by hand
│   ├── radarr.ts         Radarr's API v3, as types
│   └── sonarr.ts         Sonarr's API v3, as types
├── library/
│   └── library.ts        what movies and series share
├── queue/
│   ├── queue.ts          queueSource
│   └── definition.ts     a queue item's fields
├── radarr/
│   ├── movies.ts         radarrMovies
│   ├── queue.ts          radarrQueue
│   └── radarr.test.ts
├── sonarr/
│   ├── series.ts         sonarrSeries
│   ├── queue.ts          sonarrQueue
│   └── sonarr.test.ts
└── test/
    ├── fake-arr.ts       a fake app, as the apps' controllers answer
    └── fixtures.ts       movies and downloads, as the apps answer them

🔑 Key files

  • client/client.ts: one openapi-fetch client per app, at its URL with its key, and the app's answers as ours: a missing row is none, a refused value 400 invalid with the app's own messages.
  • library/library.ts: what Radarr's movies and Sonarr's series share: the whole library in one answer, a row read by its id, a change read first and sent back whole, a removal with the app's own choices.
  • queue/queue.ts: the queue for both apps: conditions turned into the values each param may hold in common, the size sort, the pages, and the last page the apps repeat answered as none.
  • radarr/movies.ts and sonarr/series.ts: each app's own fields, its create and update, and where its calls go. A series' counts come from its statistics.
  • test/fake-arr.ts: a fake app that filters, orders and pages its queue as the real controllers do, the last page again past the end included, so the tests hold the resources to the contract.

🧱 Built on

| Library | Used for | | --------------------------------------------------------- | -------------------------------------------------------------------------------------- | | openapi-fetch | each call, typed by the app's own paths | | openapi-typescript (dev) | writing those types from each app's OpenAPI description | | change-case | the queue's status labels: downloadClientUnavailable → "Download client unavailable" |