@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.
⚠️ 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
cryptomodule - 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/cloudRequirements
- 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 queryDimension 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) oripLocation(country, city, ISP) - Aggregations:
sum,count,avg,min,max,countDistinct - Filters: Advanced
whereclauses 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.cacheHitin 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:
- SDK extracts unique IPs from query results
- Checks which IPs are missing from database
- Fetches missing IPs from DB-IP API
- Stores in MongoDB for future queries
- Returns complete location data
Setup:
# Add to .env
DBIP_API_KEY=your_api_key_hereGet 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'andepisodeId: 'ep-001'userId: 'user-123'andcategory: 'premium'season: '2'andepisode: '5'creatorId: 'creator-789'andcontentType: '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:00Zor2024-01-15T10:30:00-05:00 - SQL format:
2024-01-15 10:30:00 - Date with slashes:
2024/01/15or01/15/2024 - Any format that
new Date()can parse
Metadata Rules
✅ DO:
- Use descriptive keys:
podcastIdis better thanpid - 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
- Never expose credentials in client-side code
- Use environment variables for credentials
- Keep credentials server-side (API routes, backends)
- 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
- Documentation: https://docs.podtoo.com
- Issues: GitHub Issues
- Email: [email protected]
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
