npm package: @podtoo/cloud
Description: Podtoo Cloud SDK for Node.js - Interact with api.cloud.podtoo.com using AWS SigV4 signing
Latest version: 0.2.13

# @podtoo/cloud

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

[![npm version](https://img.shields.io/npm/v/@podtoo/cloud.svg)](https://www.npmjs.com/package/@podtoo/cloud)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/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

```bash
npm install @podtoo/cloud
```

## Requirements

- Node.js >= 18.0.0

## Quick Start

```javascript
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:

```javascript
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:

```javascript
// 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:

```javascript
// 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:

```javascript
// 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:

```javascript
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

```javascript
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:**
```bash
# 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.

```javascript
// ✅ 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.

```javascript
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).

```javascript
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

```javascript
const builder = PodTooCloud.DataFlow("analytic");
```

#### `.query(metadata)`

Set metadata filters for the query.

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

#### `.range(timeRange)`

Set the time range for the query.

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

#### `.since(timestamp)`

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

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

#### `.groupBy(grouping)`

Group results by time period (bandwidth endpoint only).

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

#### `.limit(count)`

Limit the number of results returned.

```javascript
.limit(50)
```

#### `.orderBy(metric, direction)`

Order results by a metric (dimension endpoints).

```javascript
.orderBy("downloads", "desc")
```

#### `.metrics(metricArray)`

Specify which metrics to return (dimension endpoints).

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

#### `.forceRefresh()`

Force refresh cached data.

```javascript
.forceRefresh(true)
```

#### `.execute(customQuery?)`

Execute the query and return results.

```javascript
// 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:

```javascript
// 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

```typescript
// 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

```javascript
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

```javascript
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

```javascript
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:

```typescript
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

```javascript
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](https://docs.podtoo.com)
- **Issues**: [GitHub Issues](https://github.com/podtoo/cloud-sdk/issues)
- **Email**: support@podtoo.com

---

## License

MIT © Podtoo

---

## Changelog

### 0.2.11 (Current)
- Added Key to consumption for mulitiple files.
### 0.2.10
- Improved Where query for custom route.
- Removed AXIOS as no longer required.

### 0.2.9 
- Error with AXIOS - trying https

### 0.2.8
- 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