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 🙏

© 2025 – Pkg Stats / Ryan Hefner

lithuanian-radio-sdk

v1.0.0

Published

Production-ready TypeScript SDK for streaming Lithuanian radio stations in web applications. This package provides easy-to-use APIs for accessing and playing Lithuanian radio stations with full TypeScript support.

Downloads

17

Readme

Lithuanian Radio Stations SDK

Production-ready TypeScript SDK for streaming Lithuanian radio stations in web applications. This package provides easy-to-use APIs for accessing and playing Lithuanian radio stations with full TypeScript support.

📦 Installation

npm install lithuanian-radio-sdk

🎯 Features

  • Pre-configured Lithuanian radio stations (ZIP FM, M-1, M-1 PLIUS, M-1 DANCE, LALUNA, Lietus)
  • Web audio player with event system
  • TypeScript support with full type definitions
  • Search and filter stations by name, genre, or description
  • Custom station support - add your own stations
  • Volume control with min/max safeguards
  • Playback state management

📚 Table of Contents


Core API

The SDK provides two main classes for working with Lithuanian radio stations: LithuanianRadio for managing station data and WebPlayer for audio playback control.

LithuanianRadio Class

The main class for managing and accessing Lithuanian radio stations.

Constructor

const radio = new LithuanianRadio();

Creates a new instance with all pre-configured Lithuanian radio stations.

Methods

getStations(): RadioStation[]

Returns all available radio stations as an array.

Returns: RadioStation[] - Array of all radio stations

Example:

const stations = radio.getStations();
console.log(stations); // [{ id: 'zipfm', name: 'ZIP FM', ... }, ...]

getStation(id: string): RadioStation | undefined

Retrieves a single station by its unique ID.

Parameters:

  • id (string) - The unique identifier of the station

Returns: RadioStation | undefined - The station object or undefined if not found

Example:

const zipfm = radio.getStation("zipfm");
console.log(zipfm?.name); // "ZIP FM"

const notFound = radio.getStation("invalid-id");
console.log(notFound); // undefined

searchStations(query: string): RadioStation[]

Searches stations by name, genre, or description. Case-insensitive search.

Parameters:

  • query (string) - Search term to match against station name, genre, or description

Returns: RadioStation[] - Array of matching stations

Example:

// Search by name
const zipStations = radio.searchStations("zip");
// Returns: ZIP FM and ZIP FM (Iš Kasetės)

// Search by genre
const danceStations = radio.searchStations("dance");
// Returns: ZIP FM, M-1 DANCE, ZIP FM (Iš Kasetės)

// Search by description
const retroStations = radio.searchStations("90s");
// Returns: ZIP FM (Iš Kasetės)

addStation(station: RadioStation): void

Adds a custom radio station to the collection.

Parameters:

  • station (RadioStation) - Complete station object with all required fields

Throws: Error - If a station with the same ID already exists

Example:

radio.addStation({
  id: "my-custom-station",
  name: "My Custom Radio",
  description: "My personal radio station",
  streamUrl: "https://example.com/stream.mp3",
  website: "https://example.com",
  genre: "Various",
  country: "LT",
});

WebPlayer Class

Browser-based audio player for streaming radio stations with event handling.

Constructor

const player = new WebPlayer(options?: PlayerOptions);

Parameters:

  • options (PlayerOptions, optional) - Configuration object
    • autoplay (boolean, optional) - Enable autoplay (default: false)
    • volume (number, optional) - Initial volume 0-1 (default: 0.7)
    • preload (boolean, optional) - Preload audio (default: false)

Example:

const player = new WebPlayer({
  volume: 0.5,
  preload: true,
});

Methods

async play(station: RadioStation): Promise<void>

Starts playing the specified radio station. If a different station is already playing, it switches to the new station.

Parameters:

  • station (RadioStation) - The station to play

Returns: Promise<void> - Resolves when playback starts

Throws: Error - If playback fails

Example:

const station = radio.getStation("zipfm");
await player.play(station);

pause(): void

Pauses the current playback without clearing the station.

Example:

player.pause();

stop(): void

Stops playback completely and clears the audio source and current station.

Example:

player.stop();

setVolume(volume: number): void

Sets the playback volume with automatic clamping to valid range.

Parameters:

  • volume (number) - Volume level between 0 (mute) and 1 (max)

Note: Values outside 0-1 are automatically clamped to the valid range.

Example:

player.setVolume(0.5); // Set to 50%
player.setVolume(1); // Set to maximum
player.setVolume(0); // Mute
player.setVolume(1.5); // Clamped to 1

getVolume(): number

Gets the current volume level.

Returns: number - Current volume (0-1)

Example:

const currentVolume = player.getVolume();
console.log(currentVolume); // 0.7

isPlaying(): boolean

Checks if audio is currently playing.

Returns: boolean - True if playing, false if paused or stopped

Example:

if (player.isPlaying()) {
  console.log("Currently playing");
} else {
  console.log("Not playing");
}

getCurrentStation(): RadioStation | null

Gets the currently loaded station.

Returns: RadioStation | null - Current station or null if none is loaded

Example:

const current = player.getCurrentStation();
console.log(current?.name); // "ZIP FM"

on<K extends keyof PlayerEvents>(event: K, handler: PlayerEvents[K]): void

Registers an event listener for player events.

Parameters:

  • event (keyof PlayerEvents) - Event name ('play' | 'pause' | 'stop' | 'error' | 'volumeChange' | 'stationChange')
  • handler (Function) - Callback function for the event

Available Events:

  • play - Fired when playback starts
  • pause - Fired when playback pauses
  • stop - Fired when playback stops
  • error - Fired on errors (receives Error object)
  • volumeChange - Fired when volume changes (receives new volume)
  • stationChange - Fired when station changes (receives new RadioStation)

Example:

player.on("play", () => {
  console.log("Playback started");
});

player.on("error", (error) => {
  console.error("Player error:", error);
});

player.on("stationChange", (station) => {
  console.log("Now playing:", station.name);
});

player.on("volumeChange", (volume) => {
  console.log("Volume changed to:", volume);
});

off<K extends keyof PlayerEvents>(event: K, handler: PlayerEvents[K]): void

Removes a previously registered event listener.

Parameters:

  • event (keyof PlayerEvents) - Event name
  • handler (Function) - The exact handler function to remove

Example:

const handlePlay = () => console.log("Playing");
player.on("play", handlePlay);
player.off("play", handlePlay); // Remove the listener

destroy(): void

Cleans up all resources, stops playback, removes all event listeners, and removes the audio element.

Important: Always call this when done with the player to prevent memory leaks.

Example:

player.destroy();

Types

RadioStation

interface RadioStation {
  id: string; // Unique identifier
  name: string; // Station name
  description: string; // Station description
  streamUrl: string; // Direct stream URL
  website?: string; // Official website (optional)
  genre?: string; // Music genre (optional)
  country: string; // Country code (e.g., 'LT')
}

PlayerOptions

interface PlayerOptions {
  autoplay?: boolean; // Auto-start playback (default: false)
  volume?: number; // Initial volume 0-1 (default: 0.7)
  preload?: boolean; // Preload audio data (default: false)
}

PlayerEvents

interface PlayerEvents {
  play: () => void;
  pause: () => void;
  stop: () => void;
  error: (error: Error) => void;
  volumeChange: (volume: number) => void;
  stationChange: (station: RadioStation) => void;
}

Usage Examples

Basic Usage

import { LithuanianRadio, WebPlayer } from "lithuanian-radio-sdk";

// Initialize
const radio = new LithuanianRadio();
const player = new WebPlayer({ volume: 0.5 });

// Get all stations
const stations = radio.getStations();
console.log(`Found ${stations.length} stations`);

// Search for dance stations
const danceStations = radio.searchStations("dance");

// Play a station
const zipfm = radio.getStation("zipfm");
if (zipfm) {
  await player.play(zipfm);
}

// Volume control
player.setVolume(0.8);

// Listen to events
player.on("play", () => console.log("Started playing"));
player.on("error", (error) => console.error("Error:", error));

// Pause/Stop
player.pause();
player.stop();

// Cleanup
player.destroy();

Next.js 15+ Example

Complete implementation of a radio player component in Next.js 15+ using App Router and React Server Components.

Project Structure

app/
├── radio/
│   └── page.tsx          # Radio player page
├── components/
│   └── RadioPlayer.tsx   # Client component
└── layout.tsx

Installation

npm install lithuanian-radio-sdk

Component Implementation

app/components/RadioPlayer.tsx (Client Component)

"use client";

import { useEffect, useState, useRef } from "react";
import { LithuanianRadio, WebPlayer, RadioStation } from "lithuanian-radio-sdk";

export default function RadioPlayer() {
  const [stations, setStations] = useState<RadioStation[]>([]);
  const [currentStation, setCurrentStation] = useState<RadioStation | null>(
    null
  );
  const [isPlaying, setIsPlaying] = useState(false);
  const [volume, setVolume] = useState(0.7);
  const [searchQuery, setSearchQuery] = useState("");
  const [error, setError] = useState<string | null>(null);

  const radioRef = useRef<LithuanianRadio | null>(null);
  const playerRef = useRef<WebPlayer | null>(null);

  // Initialize radio and player
  useEffect(() => {
    radioRef.current = new LithuanianRadio();
    playerRef.current = new WebPlayer({ volume: 0.7 });

    // Load all stations
    setStations(radioRef.current.getStations());

    // Setup event listeners
    const player = playerRef.current;

    player.on("play", () => {
      setIsPlaying(true);
      setError(null);
    });

    player.on("pause", () => {
      setIsPlaying(false);
    });

    player.on("stop", () => {
      setIsPlaying(false);
      setCurrentStation(null);
    });

    player.on("error", (err: Error) => {
      setError(err.message);
      setIsPlaying(false);
    });

    player.on("stationChange", (station: RadioStation) => {
      setCurrentStation(station);
    });

    player.on("volumeChange", (vol: number) => {
      setVolume(vol);
    });

    // Cleanup on unmount
    return () => {
      player.destroy();
    };
  }, []);

  const handlePlay = async (station: RadioStation) => {
    try {
      await playerRef.current?.play(station);
    } catch (err: unknown) {
      setError("Failed to play station");
    }
  };

  const handlePause = () => {
    playerRef.current?.pause();
  };

  const handleStop = () => {
    playerRef.current?.stop();
  };

  const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newVolume = parseFloat(e.target.value);
    playerRef.current?.setVolume(newVolume);
  };

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    const query = e.target.value;
    setSearchQuery(query);

    if (query.trim() === "") {
      setStations(radioRef.current?.getStations() || []);
    } else {
      setStations(radioRef.current?.searchStations(query) || []);
    }
  };

  return (
    <div className="min-h-screen bg-linear-to-br from-blue-50 to-indigo-100 p-8">
      <div className="max-w-4xl mx-auto">
        {/* Header */}
        <div className="bg-white rounded-lg shadow-lg p-6 mb-6">
          <h1 className="text-3xl font-bold text-gray-800 mb-2">
            Lithuanian Radio Stations
          </h1>
          <p className="text-gray-600">
            Stream your favorite Lithuanian radio stations
          </p>
        </div>

        {/* Search Bar */}
        <div className="bg-white rounded-lg shadow-lg p-6 mb-6">
          <input
            type="text"
            placeholder="Search stations by name, genre, or description..."
            value={searchQuery}
            onChange={handleSearch}
            className="w-full px-4 py-2 text-gray-800 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
          />
        </div>

        {/* Now Playing */}
        {currentStation && (
          <div className="bg-linear-to-r from-indigo-500 to-purple-600 rounded-lg shadow-lg p-6 mb-6 text-white">
            <div className="flex items-center justify-between">
              <div className="flex-1">
                <p className="text-sm opacity-90 mb-1">Now Playing</p>
                <h2 className="text-2xl font-bold mb-2">
                  {currentStation.name}
                </h2>
                <p className="text-sm opacity-90 mb-2">
                  {currentStation.description}
                </p>
                {currentStation.genre && (
                  <span className="inline-block bg-white/20 px-3 py-1 rounded-full text-xs">
                    {currentStation.genre}
                  </span>
                )}
              </div>
              <div className="flex gap-3">
                {isPlaying ? (
                  <button
                    onClick={handlePause}
                    className="bg-white text-indigo-600 p-4 rounded-full hover:bg-gray-100 transition"
                  >
                    <svg
                      className="w-6 h-6"
                      fill="currentColor"
                      viewBox="0 0 20 20"
                    >
                      <path
                        fillRule="evenodd"
                        d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
                        clipRule="evenodd"
                      />
                    </svg>
                  </button>
                ) : (
                  <button
                    onClick={() => handlePlay(currentStation)}
                    className="bg-white text-indigo-600 p-4 rounded-full hover:bg-gray-100 transition"
                  >
                    <svg
                      className="w-6 h-6"
                      fill="currentColor"
                      viewBox="0 0 20 20"
                    >
                      <path
                        fillRule="evenodd"
                        d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
                        clipRule="evenodd"
                      />
                    </svg>
                  </button>
                )}
                <button
                  onClick={handleStop}
                  className="bg-white text-indigo-600 p-4 rounded-full hover:bg-gray-100 transition"
                >
                  <svg
                    className="w-6 h-6"
                    fill="currentColor"
                    viewBox="0 0 20 20"
                  >
                    <path
                      fillRule="evenodd"
                      d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z"
                      clipRule="evenodd"
                    />
                  </svg>
                </button>
              </div>
            </div>

            {/* Volume Control */}
            <div className="mt-4 flex items-center gap-3">
              <input
                type="range"
                min="0"
                max="1"
                step="0.01"
                value={volume}
                onChange={handleVolumeChange}
                className="flex-1 h-2 bg-white/30 rounded-lg appearance-none cursor-pointer"
              />
              <span className="text-sm font-medium w-12 text-right">
                {Math.round(volume * 100)}%
              </span>
            </div>
          </div>
        )}

        {/* Error Message */}
        {error && (
          <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg mb-6">
            <p className="font-medium">Error: {error}</p>
          </div>
        )}

        {/* Station List */}
        <div className="grid gap-4">
          {stations.map((station) => (
            <div
              key={station.id}
              className={`bg-white rounded-lg shadow-md p-6 transition hover:shadow-lg ${
                currentStation?.id === station.id
                  ? "ring-2 ring-indigo-500"
                  : ""
              }`}
            >
              <div className="flex items-center justify-between">
                <div className="flex-1">
                  <h3 className="text-xl font-semibold text-gray-800 mb-2">
                    {station.name}
                  </h3>
                  <p className="text-gray-600 text-sm mb-3">
                    {station.description}
                  </p>
                  <div className="flex gap-2 flex-wrap">
                    {station.genre && (
                      <span className="bg-indigo-100 text-indigo-800 px-3 py-1 rounded-full text-xs font-medium">
                        {station.genre}
                      </span>
                    )}
                    {station.website && (
                      <a
                        href={station.website}
                        target="_blank"
                        rel="noopener noreferrer"
                        className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full flex items-center justify-center text-xs font-medium hover:bg-gray-200 transition"
                      >
                        🌐 Website
                      </a>
                    )}
                  </div>
                </div>
                <button
                  onClick={() => handlePlay(station)}
                  disabled={isPlaying && currentStation?.id === station.id}
                  className={`ml-4 px-6 py-3 rounded-lg font-medium transition ${
                    isPlaying && currentStation?.id === station.id
                      ? "bg-gray-200 text-gray-500 cursor-not-allowed"
                      : "bg-indigo-600 text-white hover:bg-indigo-700"
                  }`}
                >
                  {isPlaying && currentStation?.id === station.id
                    ? "Playing"
                    : "Play"}
                </button>
              </div>
            </div>
          ))}
        </div>

        {stations.length === 0 && (
          <div className="bg-white rounded-lg shadow-lg p-12 text-center">
            <p className="text-gray-500 text-lg">
              No stations found for "{searchQuery}"
            </p>
          </div>
        )}
      </div>
    </div>
  );
}

app/radio/page.tsx (Server Component)

import RadioPlayer from "../components/RadioPlayer";

export default function RadioPage() {
  return <RadioPlayer />;
}

app/layout.tsx

import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Lithuanian Radio Player",
  description: "Stream Lithuanian radio stations online",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Features in the Next.js Example

  • Full player controls - Play, pause, and stop
  • Volume slider - Adjustable volume with visual feedback
  • Real-time search - Filter stations dynamically
  • Error handling - User-friendly error messages
  • Responsive design - Works on all screen sizes
  • Visual feedback - Currently playing station highlighted
  • Event-driven updates - Reactive state management
  • Proper cleanup - Memory leak prevention
  • TypeScript support - Full type safety
  • Tailwind CSS styling - Modern, beautiful UI

Running the Next.js App

# Navigate to your Next.js project
cd your-nextjs-app

# Install dependencies
npm install lithuanian-radio-sdk

# Run development server
npm run dev

Visit http://localhost:3000/radio to see your radio player in action!


Available Radio Stations

| ID | Name | Genre | Description | | -------------- | ------------------- | ---------------------- | ------------------------------------------------------ | | zipfm | ZIP FM | Dance/Pop/Electronic | Youth-oriented station with global dance-pop hits | | zipfm-kasete | ZIP FM (Iš Kasetės) | Retro/Dance/90s-Club | 90s/00s club anthems and nostalgic hits | | m1 | M-1 | Pop/Top 40 | First commercial station (since 1989), mainstream hits | | m1-plius | M-1 PLIUS | Adult Contemporary | Softer hits and easy-listening favorites | | m1-dance | M-1 DANCE | Electronic/House/Dance | High-energy club-ready house and electronic | | m1-laluna | LALUNA | Pop/Top 40/Feel-Good | Klaipėda's feel-good pop with beachy vibes | | m1-lietus | Lietus | Lithuanian Pop/Classic | Lithuanian-language domestic classics |


📝 License

MIT License - See LICENSE file for details

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

👨‍💻 Author

Kasparas Mickevičius

📮 Repository

https://github.com/termjs/lithuanian-radio-sdk


🐛 Troubleshooting

Playback Issues

If you encounter playback issues:

  1. Check CORS settings on your domain
  2. Ensure HTTPS is used in production
  3. Verify stream URLs are accessible
  4. Check browser console for errors

Autoplay Restrictions

Modern browsers restrict autoplay. To enable:

  1. User must interact with the page first
  2. Use muted autoplay (limited use case for radio)
  3. Request user permission explicitly

Memory Leaks

Always call player.destroy() when unmounting components to prevent memory leaks:

useEffect(() => {
  const player = new WebPlayer();
  return () => player.destroy();
}, []);

Made with ❤️ for Lithuanian radio listeners