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

@dudgy/podcast-widget

v0.2.0

Published

Framework-agnostic podcast player widget with configurable player modes, theming via CSS custom properties, and optional PodcastIndex API client.

Readme

podcast-widget

Framework-agnostic podcast player widget with configurable player modes, CSS custom property theming, and an optional PodcastIndex API client.

Zero runtime dependencies. Ships ESM + CJS with full TypeScript declarations.

Install

npm install podcast-widget

Quick Start

import { AudioEngine, PlayerController, FetchEpisodeProvider, MiniPlayer, FullPlayer } from 'podcast-widget';
import 'podcast-widget/styles';

// 1. Create the core objects
const engine = new AudioEngine();
const provider = new FetchEpisodeProvider('/api/podcast-episodes', {
  storagePrefix: 'podcast',
  maxStoredPositions: 100,
  positionSaveInterval: 5000,
  skipSeconds: 30,
  episodeCacheTTL: 3600000,
});
const controller = new PlayerController(engine, provider);

// 2. Create one or more player UIs
const mini = new MiniPlayer({
  container: document.getElementById('mini-player')!,
  controller,
  controls: { playPause: true, progress: true, expand: true },
});

const full = new FullPlayer({
  container: document.getElementById('full-player')!,
  controller,
  controls: { playPause: true, skipForward: true, skipBackward: true, volume: true, speed: true, progress: true, playlist: true, download: true },
});

// 3. Wire them together
mini.on('expand', () => full.show());
full.on('close', () => full.hide());

// 4. Load episodes
controller.loadEpisodes();

Architecture

podcast-widget/
├── src/
│   ├── core/
│   │   ├── types.ts              # Episode, PlayerConfig, events, EpisodeProvider interface
│   │   ├── event-emitter.ts      # Lightweight typed event emitter
│   │   ├── audio-engine.ts       # HTMLAudioElement wrapper with seek, destroy, error handling
│   │   ├── playback-storage.ts   # localStorage for resume positions (namespaced keys)
│   │   ├── player-state.ts       # localStorage for player state + speed preference
│   │   └── episode-provider.ts   # FetchEpisodeProvider with caching + retry
│   ├── ui/
│   │   ├── utils.ts              # formatTime, escapeHtml, getEpisodeImage
│   │   ├── html-templates.ts     # SVG icons, playlist rendering, speed options
│   │   ├── player-controller.ts  # Orchestrator: engine + storage + provider → events
│   │   ├── mini-player.ts        # Mini mode with configurable controls + inline playlist
│   │   └── full-player.ts        # Full modal with hero, playlist, keyboard shortcuts
│   ├── podcast-index/
│   │   ├── auth.ts               # HMAC-SHA1 auth headers (server-only, uses Node crypto)
│   │   ├── client.ts             # Typed PodcastIndexClient
│   │   └── index.ts              # Barrel export
│   ├── styles/
│   │   └── podcast-player.css    # Plain CSS with custom property theming (pw- prefix)
│   └── index.ts                  # Main barrel export
├── package.json                  # Dual ESM/CJS, subpath exports
├── tsconfig.json                 # Client-side TS config
├── tsconfig.server.json          # Server-side (podcast-index) config
└── tsup.config.ts                # Build config

Player Modes

The package provides two player modes that can be used independently or together. Each mode accepts a controls config to toggle which controls are visible.

Controls Config

All controls default to false (opt-in):

interface ControlsConfig {
  playPause?: boolean;
  skipForward?: boolean;
  skipBackward?: boolean;
  volume?: boolean;
  speed?: boolean;
  progress?: boolean;
  playlist?: boolean;   // Inline dropdown playlist (mini) or full playlist (full)
  expand?: boolean;     // Button to emit 'expand' event
  download?: boolean;   // Download button for current episode
}

MiniPlayer

Compact horizontal bar. Defaults: playPause: true, progress: true.

const mini = new MiniPlayer({
  container: document.getElementById('mini-player')!,
  controller,
  controls: {
    playPause: true,
    progress: true,
    volume: true,
    playlist: true,  // Adds inline dropdown playlist
    expand: true,    // Adds expand button
  },
});

mini.on('expand', () => { /* open full player, etc. */ });
mini.destroy(); // Cleanup when done

FullPlayer

Modal overlay with hero section (blurred artwork background), controls, and scrollable playlist. Defaults: all controls enabled. Each playlist item includes a download button.

const full = new FullPlayer({
  container: document.getElementById('full-player')!,
  backdrop: document.getElementById('backdrop'),  // Optional separate backdrop element
  controller,
});

full.show();  // Open modal
full.hide();  // Close modal
full.on('close', () => { /* cleanup, etc. */ });
full.destroy();

Keyboard shortcuts (when full player is visible):

  • Space / k — play/pause
  • ArrowLeft — skip backward
  • ArrowRight — skip forward
  • Escape — close

Core API

AudioEngine

Wraps HTMLAudioElement with typed events and proper cleanup.

const engine = new AudioEngine(30); // skip seconds (default: 30)

engine.load(url, startPosition?);
await engine.play();
engine.pause();
engine.stop();
engine.skipForward();
engine.skipBackward();
engine.seek(120);            // Seek to 120 seconds
engine.seekToPercentage(50); // Seek to 50%

engine.volume = 0.8;
engine.playbackRate = 1.5;

engine.on('play', () => {});
engine.on('pause', ({ currentTime }) => {});
engine.on('time-update', ({ currentTime, duration, percentage }) => {});
engine.on('ended', () => {});
engine.on('error', ({ error, context }) => {});

engine.destroy(); // Removes all listeners, releases audio element

PlayerController

Orchestrates AudioEngine, PlaybackStorage, and EpisodeProvider. Manages episode state, resume positions, and player preferences.

const controller = new PlayerController(engine, provider, {
  storagePrefix: 'podcast',       // localStorage key prefix (default: "podcast")
  maxStoredPositions: 100,         // Max saved resume positions (default: 100)
  positionSaveInterval: 5000,      // Save interval in ms (default: 5000)
  skipSeconds: 30,                 // Skip amount (default: 30)
  episodeCacheTTL: 3600000,        // Cache TTL in ms (default: 1 hour)
});

await controller.loadEpisodes();
controller.loadEpisode(0);         // Load without playing
controller.playEpisode(2);         // Load and play
controller.setSpeed(1.5);
controller.setVolume(0.8);

controller.on('episode-change', ({ episode, index }) => {});
controller.on('episodes-loaded', ({ episodes }) => {});
controller.on('episode-display', ({ imageUrl, title, show }) => {});

controller.destroy();

EpisodeProvider

Interface for loading episodes. The package ships FetchEpisodeProvider which fetches from a URL with caching and retry.

// Use the built-in fetch provider
const provider = new FetchEpisodeProvider('/api/episodes', resolvedConfig);

// Or implement your own
const customProvider: EpisodeProvider = {
  async getEpisodes() {
    return [
      { id: '1', feedId: 123, feedName: 'My Show', title: 'Ep 1', audioUrl: '...', pubDate: '...' },
    ];
  },
};

The FetchEpisodeProvider expects the endpoint to return { episodes: Episode[] }.

Episode Interface

interface Episode {
  id: string;
  feedId: number;
  feedName: string;
  title: string;
  audioUrl: string;
  pubDate: string;
  pubDateTime?: string;
  pubTimestamp?: number;
  duration?: number;
  image?: string;
  feedImage?: string;
  artwork?: string;
}

CSS Theming

Import the styles:

import 'podcast-widget/styles';

Or link directly:

<link rel="stylesheet" href="node_modules/podcast-widget/dist/podcast-player.css">

Define these CSS custom properties on a parent element to theme the player:

.my-player-wrapper {
  --player-surface: #002b36;            /* Player background */
  --player-surface-variant: #073642;    /* Secondary background */
  --player-on-surface: #839496;         /* Primary text */
  --player-on-surface-variant: #657b83; /* Secondary text */
  --player-on-surface-rgb: 131, 148, 150; /* RGB triplet for rgba() overlays */
  --player-outline: #586e75;            /* Borders */
  --player-outline-variant: #073642;    /* Subtle borders */
  --md-primary: #268bd2;               /* Accent (play button, active episode) */
  --md-on-primary: #fdf6e3;            /* Text on accent */
  --md-secondary: #2aa198;             /* Secondary accent (saved position) */
  --md-on-secondary: #fdf6e3;          /* Text on secondary accent */
}

All CSS classes use the pw- prefix to avoid collisions.

PodcastIndex Client (Server-Only)

Separate subpath export for server-side use. Uses Node's crypto module for HMAC-SHA1 auth — not included in the browser bundle.

import { PodcastIndexClient } from 'podcast-widget/podcast-index';

const client = new PodcastIndexClient(API_KEY, API_SECRET);

const episodes = await client.getEpisodes(feedId, 10);
const latest = await client.getLatestEpisode(feedId);
const feed = await client.getFeedInfo(feedId);
const results = await client.search('javascript');

You can also use the auth headers directly:

import { createPodcastIndexHeaders } from 'podcast-widget/podcast-index';

const headers = createPodcastIndexHeaders(apiKey, apiSecret);
// { 'X-Auth-Key': '...', 'X-Auth-Date': '...', Authorization: '...', 'User-Agent': '...' }

Local Development

npm run build        # Build
npm run dev          # Build + watch
npm run typecheck    # Type check
npm test             # Run tests

Testing in a Consumer Before Publishing

cd /path/to/podcast-widget
npm run build
npm link

cd /path/to/my-site
npm link podcast-widget

Now import ... from 'podcast-widget' resolves to your local build. Run npm run build in podcast-widget after changes and the consumer picks them up immediately.

When done:

cd /path/to/my-site
npm unlink podcast-widget
npm install

Publishing

npm login              # First time only
npm publish

Version bumping:

npm version patch      # 0.1.0 → 0.1.1 (bug fixes)
npm version minor      # 0.1.1 → 0.2.0 (new features)
npm version major      # 0.2.0 → 1.0.0 (breaking changes)

License

MIT