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

@podtoo/cloud

v0.2.9

Published

Podtoo Cloud SDK for Node.js - Interact with api.cloud.podtoo.com using AWS SigV4 signing

Downloads

682

Readme

@podtoo/cloud

Official Podtoo Cloud SDK for Node.js. Query and analyze your cloud storage data with AWS Signature V4 authentication.

npm version License: MIT

⚠️ Important: Server-Side Only

This package is for Node.js server environments only. It cannot be used directly in browser/client-side code because it:

  • Uses Node.js crypto module
  • Requires secret credentials that must never be exposed to browsers
  • Performs server-side AWS SigV4 signing

✅ Use in: Next.js API routes, Express servers, Node.js backends
❌ Don't use in: React components, browser JavaScript, client-side code

Installation

npm install @podtoo/cloud

Requirements

  • Node.js >= 18.0.0

Quick Start

import PodtooCloud from '@podtoo/cloud';

// Configure once with your credentials
PodtooCloud.conf({
  region: "us-east-1",
  credentials: {
    accessKeyId: process.env.PODTOO_ACCESS_KEY,
    secretAccessKey: process.env.PODTOO_SECRET_KEY,
  },
});

// Use the new DataFlow API for powerful analytics
const analytics = await PodTooCloud
  .DataFlow("analytic")
  .query({ podcastid: "show-123" })
  .range("7d")
  .execute();

console.log(`Total downloads: ${analytics.analytics.totalDownloads}`);
console.log(`Unique listeners: ${analytics.analytics.uniqueDownloads}`);
console.log(`Bandwidth: ${analytics.analytics.bandwidthGB} GB`);

🚀 DataFlow API (NEW in v0.2.4)

The DataFlow API provides a powerful, fluent interface for querying analytics across 13 different endpoints with unified caching and type safety.

Available Endpoints

| Dimension | Endpoint | Description | |-----------|----------|-------------| | Dimension Endpoints (Group by specific dimensions) | | app | /dataflow/apps | Group by podcast application | | device | /dataflow/devices | Group by device type | | os | /dataflow/oss | Group by operating system | | browser | /dataflow/browsers | Group by browser | | referrer | /dataflow/referrers | Group by referrer | | country | /dataflow/countries | Group by country | | Analytics Endpoints (Specific metrics) | | analytic | /dataflow/analytics | Comprehensive analytics | | download | /dataflow/downloads | Total downloads | | uniquedownload | /dataflow/uniquedownloads | Unique downloads (24h IP dedup) | | bandwidth | /dataflow/bandwidths | Bandwidth usage with grouping | | consumption | /dataflow/consumptions | Media consumption analytics | | Custom Endpoint (Advanced queries) | | custom | /dataflow/custom | Custom queries with MongoDB joins |

Basic Usage

All DataFlow endpoints use the same fluent API:

const result = await PodTooCloud
  .DataFlow("dimension")      // Choose endpoint
  .query({ metadata })        // Filter by metadata
  .range("timeRange")         // Set time range
  .execute();                 // Run query

Dimension Endpoints

Query analytics grouped by specific dimensions:

// Get downloads by podcast app
const apps = await PodTooCloud
  .DataFlow("app")
  .query({ podcastid: "show-123" })
  .range("30d")
  .limit(50)
  .execute();

// Response
{
  apps: [
    { app: { name: "Apple Podcasts" }, downloads: 15234, bandwidth: 45678900000 },
    { app: { name: "Spotify" }, downloads: 8432, bandwidth: 25123400000 },
    // ...
  ]
}

// Get downloads by device type
const devices = await PodTooCloud
  .DataFlow("device")
  .query({ episodeid: "ep-456" })
  .range("7d")
  .execute();

// Response
{
  devices: [
    { device: { type: "mobile" }, downloads: 8234, uniqueDownloads: 5123 },
    { device: { type: "desktop" }, downloads: 4521, uniqueDownloads: 3201 },
    // ...
  ]
}

// Get downloads by country
const countries = await PodTooCloud
  .DataFlow("country")
  .query({ season: "2" })
  .range("90d")
  .limit(25)
  .execute();

// Response
{
  countries: [
    { country: "United States", downloads: 45234 },
    { country: "United Kingdom", downloads: 18432 },
    // ...
  ]
}

Available dimension endpoints:

  • .DataFlow("app") - Group by podcast application
  • .DataFlow("device") - Group by device type (mobile, desktop, tablet)
  • .DataFlow("os") - Group by operating system
  • .DataFlow("browser") - Group by browser
  • .DataFlow("referrer") - Group by referrer URL
  • .DataFlow("country") - Group by country

Analytics Endpoints

Get specific metrics without grouping:

// Comprehensive analytics
const analytics = await PodTooCloud
  .DataFlow("analytic")
  .query({ podcastid: "show-123" })
  .range("currentMonth")
  .execute();

// Response
{
  analytics: {
    totalDownloads: 45234,
    uniqueDownloads: 32145,
    uniqueIPCount: 28901,
    botDownloads: 3421,
    humanDownloads: 41813,
    bandwidthBytes: 135678900000,
    bandwidthMB: 129387.45,
    bandwidthGB: 126.35
  }
}

// Total downloads (simple count)
const downloads = await PodTooCloud
  .DataFlow("download")
  .query({ episodeid: "ep-456" })
  .range("7d")
  .execute();

// Response
{
  totalDownloads: 8432
}

// Unique downloads (24-hour IP deduplication)
const unique = await PodTooCloud
  .DataFlow("uniquedownload")
  .query({ season: "1" })
  .range("30d")
  .execute();

// Response
{
  uniqueDownloads: 15234,
  uniqueIPs: [
    {
      ip: "203.0.113.45",
      firstAccess: "2025-12-01T10:30:00.000Z",
      lastAccess: "2025-12-01T14:25:00.000Z",
      accessCount: 3
    },
    // ...
  ]
}

// Bandwidth with time grouping
const bandwidth = await PodTooCloud
  .DataFlow("bandwidth")
  .query({ podcastid: "show-123" })
  .range("30d")
  .groupBy("day")  // Group by: day, hour, month, year
  .execute();

// Response
{
  bandwidth: {
    totalGB: 234.56,
    breakdown: [
      { period: "2025-12-01", gb: 12.34, requests: 4523, uniqueIPs: 3201 },
      { period: "2025-12-02", gb: 15.67, requests: 5234, uniqueIPs: 3890 },
      // ...
    ]
  }
}

// Media consumption (session-based)
const consumption = await PodTooCloud
  .DataFlow("consumption")
  .query({ episodeid: "ep-456" })
  .range("allTime")
  .execute();

// Response
{
  consumption: {
    overallPercent: 67.5,
    overallTimeSeconds: 1215,
    quarters: [
      { quarter: 1, rangeBytes: "0-25%", percentComplete: 100 },
      { quarter: 2, rangeBytes: "25-50%", percentComplete: 90 },
      { quarter: 3, rangeBytes: "50-75%", percentComplete: 60 },
      { quarter: 4, rangeBytes: "75-100%", percentComplete: 20 }
    ],
    secondBySecond: [...]
  }
}

Custom Endpoint (Advanced)

Create powerful custom queries with MongoDB joins and flexible grouping:

// Get downloads by country (with automatic IP enrichment)
const custom = await PodTooCloud
  .DataFlow("custom")
  .query({ podcastid: "show-123" })
  .range("30d")
  .execute({
    join: [{ collection: 'ipLocation' }],
    groupBy: ['countryName'],
    aggregations: [
      { metric: 'totalDownloads', field: 'status', function: 'count' },
      { metric: 'totalBytes', field: 'bytes_sent', function: 'sum' }
    ],
    orderBy: { field: 'totalDownloads', direction: 'desc' },
    limit: 25
  });

// Response
{
  results: [
    { countryName: "United States", totalDownloads: 15234, totalBytes: 45678900000 },
    { countryName: "United Kingdom", totalDownloads: 8432, totalBytes: 25123400000 },
    // ...
  ],
  _meta: {
    ipEnrichment: {
      enriched: 145,  // New IPs added to database
      failed: 0,
      skipped: 10
    }
  }
}

// Get app usage by device type
const appsByDevice = await PodTooCloud
  .DataFlow("custom")
  .query({ season: "2" })
  .range("90d")
  .execute({
    join: [{ collection: 'dataflow_user_agent' }],
    groupBy: ['app_name', 'device_type'],
    aggregations: [
      { metric: 'sessions', field: 'status', function: 'count' },
      { metric: 'uniqueIPs', field: 'ip', function: 'countDistinct' }
    ],
    orderBy: { field: 'sessions', direction: 'desc' }
  });

// Response
{
  results: [
    { app_name: "Apple Podcasts", device_type: "mobile", sessions: 8234, uniqueIPs: 5123 },
    { app_name: "Spotify", device_type: "desktop", sessions: 6432, uniqueIPs: 4201 },
    // ...
  ]
}

Custom query features:

  • Joins: dataflow_user_agent (app, device, OS, browser) or ipLocation (country, city, ISP)
  • Aggregations: sum, count, avg, min, max, countDistinct
  • Filters: Advanced where clauses with operators
  • Multi-field grouping: Group by any combination of fields
  • IP Enrichment: Automatic IP geolocation from DB-IP API

DataFlow API Methods

All DataFlow queries support these methods:

PodTooCloud
  .DataFlow("dimension")
  
  // Set metadata filters
  .query({ podcastid: "show-123", episodeid: "ep-456" })
  
  // Set time range
  .range("24h" | "7d" | "30d" | "currentMonth" | "lastMonth" | "allTime")
  
  // Set custom start date (for range="since")
  .since("2024-01-01T00:00:00Z")
  
  // Group by time period (bandwidth only)
  .groupBy("day" | "hour" | "month" | "year")
  
  // Limit results
  .limit(50)
  
  // Order results (dimension endpoints)
  .orderBy("metric", "desc")
  
  // Specify metrics to return (dimension endpoints)
  .metrics(["downloads", "bandwidth", "uniqueDownloads"])
  
  // Force refresh cache
  .forceRefresh(true)
  
  // Execute query
  .execute()
  
  // Execute custom query (custom endpoint only)
  .execute({
    join: [...],
    groupBy: [...],
    aggregations: [...],
    where: [...],
    orderBy: {...}
  })

Caching

All DataFlow endpoints use intelligent caching:

  • 4-hour TTL: Cached results expire after 4 hours
  • Shared cache: All endpoints share the same cache
  • Request deduplication: Concurrent requests don't duplicate work
  • Cache metadata: Check _meta.cacheHit in responses
const result = await PodTooCloud.DataFlow("analytic")
  .query({ podcastid: "show-123" })
  .range("7d")
  .execute();

// Check if cached
console.log(result._meta.cacheHit);  // true or false
console.log(result._meta.responseTime);  // milliseconds

// First request: 2-7 seconds (fetches data)
// Cached request: 300-800ms (returns cached)

IP Enrichment (Custom Endpoint)

When using the custom endpoint with ipLocation joins, IPs are automatically enriched:

  1. SDK extracts unique IPs from query results
  2. Checks which IPs are missing from database
  3. Fetches missing IPs from DB-IP API
  4. Stores in MongoDB for future queries
  5. Returns complete location data

Setup:

# Add to .env
DBIP_API_KEY=your_api_key_here

Get your API key from: https://db-ip.com/api/


🔑 Metadata System

What is Metadata?

Metadata are custom key-value pairs you attach to your uploads to organize and query them later. Think of them as tags or labels for your files.

Examples:

  • podcastId: 'my-podcast' and episodeId: 'ep-001'
  • userId: 'user-123' and category: 'premium'
  • season: '2' and episode: '5'
  • creatorId: 'creator-789' and contentType: 'audio'
  • uploadedAt: '2024-01-15T10:30:00Z' - Timestamps work too!

✨ Automatic Timestamp Conversion

The SDK automatically converts date/timestamp values to Unix timestamps! This means you can use standard date formats without worrying about colons breaking the system.

// ✅ ALL OF THESE WORK - Automatically converted to Unix timestamps
const results = await PodTooCloud
  .DataFlow("download")
  .query({ 
    podcastid: "show-123",
    uploadedAt: '2024-01-15T10:30:00Z'        // ISO 8601
  })
  .range("30d")
  .execute();

Supported date formats:

  • ISO 8601: 2024-01-15T10:30:00Z or 2024-01-15T10:30:00-05:00
  • SQL format: 2024-01-15 10:30:00
  • Date with slashes: 2024/01/15 or 01/15/2024
  • Any format that new Date() can parse

Metadata Rules

✅ DO:

  • Use descriptive keys: podcastId is better than pid
  • Be consistent: Always use the same key names
  • Use timestamps in any standard format - they're auto-converted!
  • Use alphanumeric characters, hyphens, and underscores

❌ DON'T:

  • Use colons (:) in non-timestamp metadata values
  • Use special characters like &, =, ? in keys or values

API Reference

Configuration

conf(config)

Initialize the SDK with your credentials. Call this once when your application starts.

PodtooCloud.conf({
  region: "us-east-1",  // Optional, defaults to us-east-1
  credentials: {
    accessKeyId: "YOUR_ACCESS_KEY",
    secretAccessKey: "YOUR_SECRET_KEY",
  },
});

setCredentials(accessKeyId, secretAccessKey)

Update credentials at runtime (useful for multi-tenant applications).

PodtooCloud.setCredentials(
  "NEW_ACCESS_KEY",
  "NEW_SECRET_KEY"
);

DataFlow Methods

.DataFlow(dimension)

Create a new DataFlow query builder.

Dimension endpoints:

  • "app" - Group by podcast application
  • "device" - Group by device type
  • "os" - Group by operating system
  • "browser" - Group by browser
  • "referrer" - Group by referrer
  • "country" - Group by country

Analytics endpoints:

  • "analytic" - Comprehensive analytics
  • "download" - Total downloads
  • "uniquedownload" - Unique downloads
  • "bandwidth" - Bandwidth with grouping
  • "consumption" - Media consumption

Custom endpoint:

  • "custom" - Custom queries with joins
const builder = PodTooCloud.DataFlow("analytic");

.query(metadata)

Set metadata filters for the query.

.query({ 
  podcastid: "show-123",
  episodeid: "ep-456"
})

.range(timeRange)

Set the time range for the query.

.range("7d")  // "24h", "7d", "30d", "currentMonth", "lastMonth", "allTime"

.since(timestamp)

Set a custom start date (requires range: "since").

.since("2024-01-01T00:00:00Z")

.groupBy(grouping)

Group results by time period (bandwidth endpoint only).

.groupBy("day")  // "day", "hour", "month", "year"

.limit(count)

Limit the number of results returned.

.limit(50)

.orderBy(metric, direction)

Order results by a metric (dimension endpoints).

.orderBy("downloads", "desc")

.metrics(metricArray)

Specify which metrics to return (dimension endpoints).

.metrics(["downloads", "bandwidth", "uniqueDownloads"])

.forceRefresh()

Force refresh cached data.

.forceRefresh(true)

.execute(customQuery?)

Execute the query and return results.

// Standard execution
const result = await builder.execute();

// Custom query execution
const result = await builder.execute({
  join: [{ collection: 'ipLocation' }],
  groupBy: ['countryName'],
  aggregations: [
    { metric: 'downloads', field: 'status', function: 'count' }
  ]
});

Legacy Analytics Methods (Still Supported)

These methods are still available but we recommend using the new DataFlow API:

// Legacy
const analytics = await PodtooCloud.getAnalytics(
  { podcastid: "show-123" },
  { range: "7d" }
);

// New DataFlow API (Recommended)
const analytics = await PodTooCloud
  .DataFlow("analytic")
  .query({ podcastid: "show-123" })
  .range("7d")
  .execute();

Legacy methods:

  • getAnalytics(metadata, options)
  • getUniqueDownloads(metadata, options)
  • getTotalDownloads(metadata, options)
  • getBandwidth(metadata, options)
  • getConsumption(metadata, options)
  • queryByMetadata(metadata)

Usage Examples

Next.js API Route with DataFlow

// pages/api/podcast-stats.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import PodtooCloud from '@podtoo/cloud';

PodtooCloud.conf({
  region: "us-east-1",
  credentials: {
    accessKeyId: process.env.PODTOO_ACCESS_KEY!,
    secretAccessKey: process.env.PODTOO_SECRET_KEY!,
  },
});

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const { podcastid, range = '30d' } = req.query;

    // Parallel DataFlow queries
    const [analytics, apps, countries, bandwidth] = await Promise.all([
      PodTooCloud.DataFlow("analytic")
        .query({ podcastid })
        .range(range)
        .execute(),
      
      PodTooCloud.DataFlow("app")
        .query({ podcastid })
        .range(range)
        .limit(10)
        .execute(),
      
      PodTooCloud.DataFlow("country")
        .query({ podcastid })
        .range(range)
        .limit(25)
        .execute(),
      
      PodTooCloud.DataFlow("bandwidth")
        .query({ podcastid })
        .range(range)
        .groupBy("day")
        .execute()
    ]);

    return res.status(200).json({
      analytics,
      apps,
      countries,
      bandwidth
    });
  } catch (error: any) {
    return res.status(500).json({ error: error.message });
  }
}

Express Server with Custom Queries

import express from 'express';
import PodtooCloud from '@podtoo/cloud';

const app = express();

PodtooCloud.conf({
  region: "us-east-1",
  credentials: {
    accessKeyId: process.env.PODTOO_ACCESS_KEY,
    secretAccessKey: process.env.PODTOO_SECRET_KEY,
  },
});

// Get downloads by country with IP enrichment
app.get('/api/countries/:podcastid', async (req, res) => {
  try {
    const result = await PodTooCloud
      .DataFlow("custom")
      .query({ podcastid: req.params.podcastid })
      .range("30d")
      .execute({
        join: [{ collection: 'ipLocation' }],
        groupBy: ['countryName', 'city'],
        aggregations: [
          { metric: 'downloads', field: 'status', function: 'count' },
          { metric: 'bandwidth', field: 'bytes_sent', function: 'sum' }
        ],
        orderBy: { field: 'downloads', direction: 'desc' },
        limit: 50
      });
    
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000);

Compare Multiple Podcasts

async function comparePodcasts(podcastIds) {
  const results = await Promise.all(
    podcastIds.map(podcastid =>
      PodTooCloud.DataFlow("analytic")
        .query({ podcastid })
        .range("currentMonth")
        .execute()
    )
  );
  
  results.forEach((result, i) => {
    console.log(`\nPodcast ${podcastIds[i]}:`);
    console.log(`Downloads: ${result.analytics.totalDownloads}`);
    console.log(`Unique: ${result.analytics.uniqueDownloads}`);
    console.log(`Bandwidth: ${result.analytics.bandwidthGB} GB`);
  });
}

comparePodcasts(['show-1', 'show-2', 'show-3']);

Real-Time Dashboard Data

async function getDashboardData(podcastid) {
  const [
    today,
    week,
    month,
    apps,
    devices,
    countries
  ] = await Promise.all([
    PodTooCloud.DataFlow("analytic").query({ podcastid }).range("24h").execute(),
    PodTooCloud.DataFlow("analytic").query({ podcastid }).range("7d").execute(),
    PodTooCloud.DataFlow("analytic").query({ podcastid }).range("30d").execute(),
    PodTooCloud.DataFlow("app").query({ podcastid }).range("30d").limit(5).execute(),
    PodTooCloud.DataFlow("device").query({ podcastid }).range("30d").execute(),
    PodTooCloud.DataFlow("country").query({ podcastid }).range("30d").limit(10).execute()
  ]);
  
  return {
    overview: {
      today: today.analytics.totalDownloads,
      week: week.analytics.totalDownloads,
      month: month.analytics.totalDownloads
    },
    topApps: apps.apps.slice(0, 5),
    deviceBreakdown: devices.devices,
    topCountries: countries.countries.slice(0, 10)
  };
}

TypeScript Support

Full TypeScript definitions included:

import PodTooCloud from '@podtoo/cloud';

// Type-safe DataFlow queries
const analytics = await PodTooCloud
  .DataFlow("analytic")
  .query({ podcastid: "show-123" })
  .range("7d")
  .execute();

// Custom queries with full type safety
const custom = await PodTooCloud
  .DataFlow("custom")
  .query({ podcastid: "show-123" })
  .range("30d")
  .execute({
    join: [{ collection: 'ipLocation' }],
    groupBy: ['countryName'],
    aggregations: [
      { metric: 'downloads', field: 'status', function: 'count' }
    ]
  });

Security Best Practices

  1. Never expose credentials in client-side code
  2. Use environment variables for credentials
  3. Keep credentials server-side (API routes, backends)
  4. Use HTTPS in production

Error Handling

try {
  const result = await PodTooCloud
    .DataFlow("analytic")
    .query({ podcastid: "show-123" })
    .range("7d")
    .execute();
} catch (error) {
  if (error.message.includes('status: 401')) {
    console.error('Authentication failed');
  } else if (error.message.includes('status: 404')) {
    console.error('No data found');
  } else {
    console.error('Error:', error.message);
  }
}

Support


License

MIT © Podtoo


Changelog

0.2.9 (Current) 🎉

  • Error with AXIOS - trying https

0.2.8 (Current) 🎉

  • Error with AXIOS request method

0.2.7

  • Removed Fetch and Added AXIOS due to error with Fetch

0.2.6

  • Force IPv4 connection.

0.2.5

  • Fix GroupBy issue.

0.2.4

Added

  • 🚀 Complete DataFlow API: Unified fluent interface for all analytics
  • 13 DataFlow endpoints: 7 dimensions + 5 analytics + 1 custom
  • Dimension endpoints: app, device, os, browser, referrer, country
  • Analytics endpoints: analytic, download, uniquedownload, bandwidth, consumption
  • Custom endpoint: Flexible queries with MongoDB joins (ipLocation, dataflow_user_agent)
  • IP enrichment: Automatic IP geolocation from DB-IP API
  • 4-hour caching: All endpoints share intelligent cache with request deduplication
  • Time grouping: Bandwidth endpoint supports day/hour/month/year grouping
  • Advanced filtering: Custom queries support where clauses and multi-field grouping
  • Type safety: Full TypeScript support for all DataFlow methods

Changed

  • Legacy analytics methods still supported but DataFlow API recommended
  • All endpoints now use unified caching system
  • Improved performance: First request 2-7s, cached 300-800ms

0.2.3

  • DataFlow custom queries now supported (paid feature)

0.2.2

  • DataFlow app data (paid feature)
  • DataFlow app consumption data (paid feature)
  • DataFlow device consumption data (paid feature)

0.2.1

  • DataFlow app consumption data (paid feature)

0.2.0

Added

  • 🎉 Dual-format support: Package now works with both ESM (import) and CommonJS (require)
  • CommonJS build output in dist/cjs/
  • ESM build output in dist/esm/
  • Proper conditional exports in package.json

0.1.16

  • Added getConsumption() method for media consumption analytics
  • Quarterly breakdown for drop-off analysis
  • Second-by-second consumption data

0.1.0

  • Initial public release
  • Query by metadata
  • Comprehensive analytics
  • AWS SigV4 signing
  • TypeScript support