From 1c2dbc3165087ef28294e5fcd4291dbc6ce5cdd9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 00:43:37 +0000 Subject: [PATCH 1/2] Initial plan From 142746a2afdf73dfc19806dd2d92589da79f6266 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 00:53:58 +0000 Subject: [PATCH 2/2] Add channel dataset and Stripe + Firebase billing integration stubs Co-authored-by: Stacey77 <54900383+Stacey77@users.noreply.github.com> --- README/DEVNOTES.md | 346 +++++ docs/STRIPE_FIREBASE_INTEGRATION.md | 332 ++++ functions/README.md | 144 ++ functions/stripe-webhooks/index.js | 192 +++ src/components/ChannelLock.stubs.jsx | 152 ++ src/data/channels.full.json | 2162 ++++++++++++++++++++++++++ src/utils/loadChannels.js | 36 + 7 files changed, 3364 insertions(+) create mode 100644 README/DEVNOTES.md create mode 100644 docs/STRIPE_FIREBASE_INTEGRATION.md create mode 100644 functions/README.md create mode 100644 functions/stripe-webhooks/index.js create mode 100644 src/components/ChannelLock.stubs.jsx create mode 100644 src/data/channels.full.json create mode 100644 src/utils/loadChannels.js diff --git a/README/DEVNOTES.md b/README/DEVNOTES.md new file mode 100644 index 0000000..732a7fc --- /dev/null +++ b/README/DEVNOTES.md @@ -0,0 +1,346 @@ +# Developer Notes - Credentials and Deployment Checklist + +This document provides a comprehensive checklist for maintainers to configure credentials and environment variables required for CI/CD, deployments, and production operations. + +## Required Credentials Overview + +The following credentials are required to fully deploy and operate the application in staging and production environments. + +### Stripe Credentials + +Stripe provides separate test and live credentials. Use test credentials for development and staging, and live credentials only for production. + +#### Test Mode (Development/Staging) + +- **STRIPE_TEST_SECRET**: Secret key for Stripe API calls + - Format: Starts with `sk_test_` followed by alphanumeric characters + - Location: Stripe Dashboard → Developers → API Keys → Secret key (test mode) + - Usage: Backend API calls, webhook signature verification + +- **STRIPE_TEST_PUBLISHABLE**: Publishable key for client-side Stripe.js + - Format: Starts with `pk_test_` followed by alphanumeric characters + - Location: Stripe Dashboard → Developers → API Keys → Publishable key (test mode) + - Usage: Client-side checkout, card tokenization + +#### Live Mode (Production) + +- **STRIPE_LIVE_SECRET**: Secret key for production Stripe API calls + - Format: Starts with `sk_live_` followed by alphanumeric characters + - Location: Stripe Dashboard → Developers → API Keys → Secret key (live mode) + - Usage: Production backend API calls + - ⚠️ **CRITICAL**: Protect this key - it can charge real money + +- **STRIPE_LIVE_PUBLISHABLE**: Publishable key for production client-side + - Format: Starts with `pk_live_` followed by alphanumeric characters + - Location: Stripe Dashboard → Developers → API Keys → Publishable key (live mode) + - Usage: Production client-side checkout + +#### Webhook Secrets + +- **STRIPE_WEBHOOK_SECRET**: Webhook endpoint signing secret + - Format: Starts with `whsec_` followed by alphanumeric characters + - Location: Stripe Dashboard → Developers → Webhooks → [Your endpoint] → Signing secret + - Usage: Verify webhook authenticity + - Note: Different secret for each webhook endpoint (test vs. live, staging vs. prod) + +### Firebase Credentials + +#### Service Account + +- **FIREBASE_SERVICE_ACCOUNT**: Firebase Admin SDK service account JSON (base64 encoded) + - Format: Base64-encoded JSON file + - Location: Firebase Console → Project Settings → Service Accounts → Generate new private key + - Usage: Server-side Firebase Admin SDK initialization, Firestore access + - Encoding: `base64 -w 0 service-account.json > service-account-base64.txt` + - ⚠️ **CRITICAL**: Contains private key - NEVER commit to source control + +#### Project Configuration + +- **FIREBASE_PROJECT_ID**: Firebase project identifier + - Format: `your-project-id` + - Location: Firebase Console → Project Settings → General → Project ID + - Usage: Firebase initialization, function deployment + +### Android Build Credentials (Optional) + +Required only if building signed Android APKs. + +- **ANDROID_KEYSTORE**: Android signing keystore file (base64 encoded) + - Format: Base64-encoded .jks or .keystore file + - Generation: `keytool -genkey -v -keystore release.keystore -alias release -keyalg RSA -keysize 2048 -validity 10000` + - Encoding: `base64 -w 0 release.keystore > keystore-base64.txt` + +- **KEY_ALIAS**: Alias of the key in the keystore + - Example: `release` + +- **KEYSTORE_PASSWORD**: Password for the keystore file + - Set during keystore creation + +- **KEY_PASSWORD**: Password for the specific key + - Set during keystore creation + +## Environment Variable Configuration + +### CI/CD (GitHub Actions, GitLab CI, etc.) + +Set these as repository secrets: + +``` +STRIPE_TEST_SECRET= +STRIPE_TEST_PUBLISHABLE= +STRIPE_LIVE_SECRET= +STRIPE_LIVE_PUBLISHABLE= +STRIPE_WEBHOOK_SECRET= +FIREBASE_SERVICE_ACCOUNT= +FIREBASE_PROJECT_ID=your-project-id +ANDROID_KEYSTORE= +KEY_ALIAS=release +KEYSTORE_PASSWORD= +KEY_PASSWORD= +``` + +### Firebase Cloud Functions + +Set using Firebase CLI: + +```bash +firebase functions:config:set \ + stripe.secret_key="" \ + stripe.webhook_secret="" +``` + +Verify: +```bash +firebase functions:config:get +``` + +### Local Development + +Create a `.env` file (⚠️ **NEVER commit this file**): + +```env +# Stripe Test Keys +STRIPE_TEST_SECRET= +STRIPE_TEST_PUBLISHABLE= +STRIPE_WEBHOOK_SECRET= + +# Firebase +FIREBASE_PROJECT_ID=your-project-id +FIREBASE_SERVICE_ACCOUNT_PATH=/path/to/service-account.json + +# Android (optional) +ANDROID_KEYSTORE_PATH=/path/to/release.keystore +KEY_ALIAS=release +KEYSTORE_PASSWORD=... +KEY_PASSWORD=... +``` + +Add to `.gitignore`: +``` +.env +.env.local +*.keystore +*.jks +service-account*.json +``` + +## Setup Steps + +### 1. Open Draft Pull Request + +This PR is intentionally opened as a **draft** to allow for credential configuration and review before merging. + +**Steps:** +1. Review all files added in this PR +2. Verify no secrets are committed (use `git log -p` to review) +3. Add required credentials to CI/CD secrets +4. Configure Firebase functions environment variables +5. Mark PR as "Ready for review" once credentials are configured + +### 2. Stripe Configuration + +**Create Products and Prices:** + +```bash +# Using Stripe CLI +stripe products create --name="Silver Tier" --description="Premium channels, HD streaming" +stripe prices create --product=prod_XXX --unit-amount=999 --currency=usd --recurring.interval=month + +stripe products create --name="Gold Tier" --description="All channels, HD + 4K, offline downloads" +stripe prices create --product=prod_YYY --unit-amount=1999 --currency=usd --recurring.interval=month +``` + +**Configure Webhook:** + +1. Go to Stripe Dashboard → Developers → Webhooks +2. Add endpoint: `https://YOUR_FUNCTION_URL/webhook` +3. Select events: + - `invoice.payment_succeeded` + - `invoice.payment_failed` + - `customer.subscription.created` + - `customer.subscription.updated` + - `customer.subscription.deleted` +4. Copy signing secret and add to Firebase config + +### 3. Firebase Deployment + +**Initial Setup:** + +```bash +# Install dependencies +cd functions +npm install + +# Set environment variables +firebase functions:config:set \ + stripe.secret_key="" \ + stripe.webhook_secret="" + +# Deploy functions +firebase deploy --only functions +``` + +**Firestore Security Rules:** + +Update `firestore.rules` to protect entitlements: + +```javascript +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /entitlements/{uid} { + allow read: if request.auth != null && request.auth.uid == uid; + allow write: if false; + } + match /users/{uid}/subscriptions/{subId} { + allow read: if request.auth != null && request.auth.uid == uid; + allow write: if false; + } + } +} +``` + +Deploy rules: +```bash +firebase deploy --only firestore:rules +``` + +### 4. Staging Verification + +Test the following in staging environment: + +- [ ] User can create checkout session +- [ ] Stripe Checkout page loads correctly +- [ ] Test payment succeeds (use test card: 4242 4242 4242 4242) +- [ ] Webhook receives `invoice.payment_succeeded` +- [ ] Firestore entitlements update correctly +- [ ] Client app reflects new subscription tier +- [ ] Premium channels unlock for gold tier users +- [ ] Subscription management (upgrade, downgrade, cancel) works + +### 5. Production Promotion + +**Pre-production Checklist:** + +- [ ] All staging tests pass +- [ ] Stripe products and prices created in live mode +- [ ] Live webhook endpoint configured +- [ ] Firebase functions config updated with live keys +- [ ] Firestore security rules deployed +- [ ] Monitoring and alerting configured +- [ ] Customer support team briefed on subscription flows + +**Deployment:** + +```bash +# Update to live Stripe keys +firebase functions:config:set \ + stripe.secret_key="" \ + stripe.webhook_secret="" + +# Deploy with live configuration +firebase deploy --only functions + +# Verify deployment +curl https://YOUR_PROD_FUNCTION_URL/health +``` + +**Post-deployment:** + +- [ ] Verify webhook endpoint in Stripe Dashboard (live mode) +- [ ] Test with real card in production (small amount, then refund) +- [ ] Monitor Cloud Function logs for first few hours +- [ ] Set up alerts for failed webhooks or errors + +## Maintenance Tasks + +### Rotating Secrets + +**Stripe Webhook Secret:** + +1. Create new webhook endpoint in Stripe Dashboard +2. Update Firebase config with new secret +3. Deploy functions: `firebase deploy --only functions` +4. Delete old webhook endpoint after verifying new one works + +**Firebase Service Account:** + +1. Generate new service account key in Firebase Console +2. Update CI/CD secret with new base64-encoded JSON +3. Trigger new deployment +4. Revoke old service account key + +### Monitoring + +Set up alerts for: +- Failed webhook deliveries (Stripe Dashboard) +- Cloud Function errors (Firebase Console → Functions) +- Failed payment attempts (Stripe Dashboard → Payments) +- Firestore write errors (Firebase Console → Firestore) + +## Troubleshooting + +### Webhook Signature Verification Fails + +- Verify `STRIPE_WEBHOOK_SECRET` matches the endpoint in Stripe Dashboard +- Check endpoint URL exactly matches (including trailing slash) +- Ensure `bodyParser.raw()` is used, not `bodyParser.json()` + +### Entitlements Not Updating + +- Check Cloud Function logs: `firebase functions:log` +- Verify customer metadata includes `firebaseUid` +- Check Firestore security rules allow function to write +- Verify webhook events are being delivered (Stripe Dashboard) + +### Android Build Fails + +- Verify keystore base64 decoding: `echo $ANDROID_KEYSTORE | base64 -d > test.keystore` +- Check keystore password and alias are correct +- Ensure keystore file is not corrupted + +## Security Best Practices + +- ✅ Store all secrets in environment variables or secure vaults +- ✅ Use different credentials for test, staging, and production +- ✅ Rotate secrets periodically (at least annually) +- ✅ Limit access to production credentials to essential personnel only +- ✅ Enable audit logging for credential access +- ✅ Use least-privilege access for service accounts +- ✅ Monitor for unauthorized access or unusual activity +- ❌ NEVER commit secrets to source control +- ❌ NEVER share secrets via email or chat +- ❌ NEVER use production credentials in development/testing + +## Contact + +For access to credentials or deployment permissions, contact: + +- **Stripe Access**: [Project Owner] - for API keys and webhook configuration +- **Firebase Access**: [Project Owner] - for service account and deployer permissions +- **Android Signing**: [Build Team Lead] - for keystore access + +--- + +**Last Updated**: 2024-12-25 +**Maintained By**: Development Team diff --git a/docs/STRIPE_FIREBASE_INTEGRATION.md b/docs/STRIPE_FIREBASE_INTEGRATION.md new file mode 100644 index 0000000..9b6be65 --- /dev/null +++ b/docs/STRIPE_FIREBASE_INTEGRATION.md @@ -0,0 +1,332 @@ +# Stripe + Firebase Integration Guide + +This document outlines the integration architecture between Stripe (payment processing) and Firebase (backend services) for subscription-based channel access. + +## Overview + +The integration enables: +- Subscription management through Stripe +- User entitlement tracking in Firestore +- Automated webhook processing to sync subscription state +- Client-side access control based on subscription tiers + +## Architecture Flow + +``` +┌─────────┐ ┌──────────────┐ ┌────────┐ ┌──────────┐ +│ Client │─────>│ Cloud │─────>│ Stripe │─────>│ Stripe │ +│ App │ │ Function │ │ API │ │ Checkout │ +└─────────┘ └──────────────┘ └────────┘ └──────────┘ + ↑ │ + │ ↓ + │ ┌──────────────┐ ┌────────┐ ┌──────────┐ + └───────────│ Firestore │<─────│ Cloud │<─────│ Webhook │ + │ Entitle- │ │Function│ │ Event │ + │ ments │ └────────┘ └──────────┘ + └──────────────┘ +``` + +### Flow Steps + +1. **Client** requests subscription checkout session via Cloud Function +2. **Cloud Function** creates Stripe Checkout Session with customer metadata +3. **Stripe Checkout** handles payment UI and processing +4. **Stripe Webhooks** notify our webhook endpoint about events +5. **Webhook Function** processes events and updates Firestore +6. **Client** reads entitlements from Firestore in real-time + +## Firestore Schema + +### Collection: `/users/{uid}` + +User profile and metadata. + +```json +{ + "email": "user@example.com", + "displayName": "John Doe", + "stripeCustomerId": "cus_XXXXXXXXXX", + "createdAt": "2024-01-01T00:00:00Z" +} +``` + +### Collection: `/users/{uid}/subscriptions/{subscriptionId}` + +Individual subscription records. + +```json +{ + "subscriptionId": "sub_XXXXXXXXXX", + "status": "active", + "tier": "gold", + "priceId": "price_XXXXXXXXXX", + "productId": "prod_XXXXXXXXXX", + "currentPeriodStart": "2024-01-01T00:00:00Z", + "currentPeriodEnd": "2024-02-01T00:00:00Z", + "cancelAtPeriodEnd": false, + "createdAt": "2024-01-01T00:00:00Z", + "updatedAt": "2024-01-15T00:00:00Z" +} +``` + +### Collection: `/entitlements/{uid}` + +Denormalized entitlement data for fast client reads. + +```json +{ + "tier": "gold", + "validUntil": "2024-02-01T00:00:00Z", + "subscriptionId": "sub_XXXXXXXXXX", + "status": "active", + "features": ["premium_channels", "hd_streaming", "offline_downloads"], + "updatedAt": "2024-01-15T00:00:00Z" +} +``` + +**Why denormalize?** The `/entitlements/{uid}` collection provides a single document read for client access control, avoiding the need to query subcollections. + +## Stripe Customer Metadata Mapping + +When creating a Stripe Customer, include Firebase UID in metadata: + +```javascript +const customer = await stripe.customers.create({ + email: user.email, + metadata: { + firebaseUid: user.uid, // CRITICAL: Map Stripe customer to Firebase user + }, +}); +``` + +This mapping allows webhook handlers to update the correct Firestore documents when Stripe events occur. + +## Recommended Implementation Flow + +### 1. Create Checkout Session (Client → Cloud Function) + +**Client Code:** +```javascript +const createCheckoutSession = async (priceId) => { + const response = await fetch('/api/create-checkout-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ priceId }), + }); + const { sessionId } = await response.json(); + + const stripe = await loadStripe('YOUR_PUBLISHABLE_KEY'); + await stripe.redirectToCheckout({ sessionId }); +}; +``` + +**Cloud Function:** +```javascript +exports.createCheckoutSession = functions.https.onCall(async (data, context) => { + // Verify authentication + if (!context.auth) { + throw new functions.https.HttpsError('unauthenticated', 'Must be logged in'); + } + + const { priceId } = data; + const uid = context.auth.uid; + const user = await admin.auth().getUser(uid); + + // Get or create Stripe customer + let customerId = await getStripeCustomerId(uid); + if (!customerId) { + const customer = await stripe.customers.create({ + email: user.email, + metadata: { firebaseUid: uid }, + }); + customerId = customer.id; + await admin.firestore().collection('users').doc(uid).update({ + stripeCustomerId: customerId, + }); + } + + // Create checkout session + const session = await stripe.checkout.sessions.create({ + customer: customerId, + payment_method_types: ['card'], + line_items: [{ price: priceId, quantity: 1 }], + mode: 'subscription', + success_url: 'https://yourapp.com/success', + cancel_url: 'https://yourapp.com/cancel', + }); + + return { sessionId: session.id }; +}); +``` + +### 2. Process Webhook Events + +See `functions/stripe-webhooks/index.js` for webhook implementation. + +Key events to handle: +- `customer.subscription.created` - Initialize subscription in Firestore +- `invoice.payment_succeeded` - Activate/renew entitlements +- `invoice.payment_failed` - Handle failed payments +- `customer.subscription.updated` - Update tier, status changes +- `customer.subscription.deleted` - Revoke entitlements + +### 3. Client-Side Entitlement Checks + +**React Hook:** +```javascript +const useUserEntitlements = () => { + const [entitlements, setEntitlements] = useState(null); + const { user } = useAuth(); + + useEffect(() => { + if (!user) return; + + const unsubscribe = firestore + .collection('entitlements') + .doc(user.uid) + .onSnapshot(doc => { + setEntitlements(doc.data()); + }); + + return unsubscribe; + }, [user]); + + return entitlements; +}; +``` + +**Component Usage:** +```javascript +import ChannelLock from './components/ChannelLock.stubs'; + +const ChannelPlayer = ({ channel }) => { + return ( + + + + ); +}; +``` + +## Security Considerations + +### Webhook Signature Verification + +**ALWAYS** verify webhook signatures to prevent spoofing: + +```javascript +const event = stripe.webhooks.constructEvent( + req.body, + req.headers['stripe-signature'], + process.env.STRIPE_WEBHOOK_SECRET +); +``` + +### Firestore Security Rules + +Protect entitlement data with proper security rules: + +```javascript +// firestore.rules +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // Users can read their own entitlements + match /entitlements/{uid} { + allow read: if request.auth != null && request.auth.uid == uid; + allow write: if false; // Only Cloud Functions can write + } + + // Users can read their own subscriptions + match /users/{uid}/subscriptions/{subId} { + allow read: if request.auth != null && request.auth.uid == uid; + allow write: if false; // Only Cloud Functions can write + } + } +} +``` + +### Environment Variables + +- **NEVER** commit secrets to source control +- Use Firebase Functions config for secrets +- Use different keys for test/production environments +- Rotate webhook secrets periodically + +## Subscription Tiers + +Define tiers based on Stripe Products/Prices: + +| Tier | Price ID | Features | +|--------|-------------------|---------------------------------------------| +| Free | N/A | Basic channels, SD quality | +| Silver | `price_silver_*` | Premium channels, HD quality | +| Gold | `price_gold_*` | All channels, HD + 4K, offline downloads | + +Map price IDs to tiers in webhook handlers: + +```javascript +const TIER_MAPPING = { + 'price_silver_monthly': 'silver', + 'price_silver_yearly': 'silver', + 'price_gold_monthly': 'gold', + 'price_gold_yearly': 'gold', +}; + +const tier = TIER_MAPPING[subscription.items.data[0].price.id] || 'free'; +``` + +## Testing + +### Test Mode + +1. Use Stripe test mode (keys starting with sk_test_ and pk_test_) +2. Use test card: `4242 4242 4242 4242` +3. Use Stripe CLI to trigger webhook events locally + +### Webhook Testing + +```bash +stripe listen --forward-to https://YOUR_FUNCTION_URL/webhook +stripe trigger customer.subscription.created +stripe trigger invoice.payment_succeeded +``` + +## Production Checklist + +- [ ] Switch to live Stripe keys (starts with sk_live_ and pk_live_) +- [ ] Update webhook endpoint URL in Stripe Dashboard +- [ ] Configure production webhook secret +- [ ] Set up Firestore Security Rules +- [ ] Enable Firestore backup and point-in-time recovery +- [ ] Set up monitoring and alerting for failed payments +- [ ] Test subscription flows in production environment +- [ ] Document customer support procedures for subscription issues + +## Troubleshooting + +### Webhooks Not Received + +- Verify webhook URL is correct and publicly accessible +- Check Stripe Dashboard → Webhooks for delivery status +- Review Cloud Function logs: `firebase functions:log` + +### Entitlements Not Updating + +- Check webhook signature verification passes +- Verify customer metadata includes `firebaseUid` +- Check Firestore permissions and rules +- Review Cloud Function logs for errors + +### Payment Fails but Subscription Created + +- This is normal - Stripe may retry +- Handle `invoice.payment_failed` to notify user +- Implement grace period logic if desired + +## References + +- [Stripe API Documentation](https://stripe.com/docs/api) +- [Firebase Cloud Functions](https://firebase.google.com/docs/functions) +- [Stripe Checkout](https://stripe.com/docs/payments/checkout) +- [Stripe Webhooks Best Practices](https://stripe.com/docs/webhooks/best-practices) diff --git a/functions/README.md b/functions/README.md new file mode 100644 index 0000000..a9a917b --- /dev/null +++ b/functions/README.md @@ -0,0 +1,144 @@ +# Firebase Cloud Functions - Deployment Guide + +This directory contains Firebase Cloud Functions for the Stripe integration. + +## Prerequisites + +1. **Firebase CLI**: Install the Firebase CLI if you haven't already + ```bash + npm install -g firebase-tools + ``` + +2. **Firebase Project**: Ensure you have a Firebase project set up + ```bash + firebase login + firebase init functions # If not already initialized + ``` + +3. **Stripe Account**: Set up a Stripe account and obtain your API keys + +## Environment Configuration + +Firebase Cloud Functions use environment configuration for sensitive data like API keys. + +### Required Environment Variables + +Set the following configuration values using the Firebase CLI: + +```bash +# Stripe Secret Key (from Stripe Dashboard → Developers → API Keys) +firebase functions:config:set stripe.secret_key="" + +# Stripe Webhook Secret (from Stripe Dashboard → Developers → Webhooks) +firebase functions:config:set stripe.webhook_secret="" +``` + +### Verify Configuration + +Check your current configuration: + +```bash +firebase functions:config:get +``` + +## Deployment + +### Deploy All Functions + +```bash +firebase deploy --only functions +``` + +### Deploy Specific Function + +```bash +firebase deploy --only functions:stripeWebhook +``` + +## Complete Setup Sequence + +Follow these steps for initial setup: + +```bash +# 1. Set environment variables +firebase functions:config:set stripe.secret_key="" +firebase functions:config:set stripe.webhook_secret="" + +# 2. Install dependencies +cd functions +npm install + +# 3. Deploy functions +firebase deploy --only functions + +# 4. Note the deployed function URL (e.g., https://us-central1-PROJECT.cloudfunctions.net/stripeWebhook) + +# 5. Configure webhook in Stripe Dashboard: +# - Go to Stripe Dashboard → Developers → Webhooks +# - Add endpoint with your function URL + /webhook path +# - Select events to listen for: invoice.*, customer.subscription.* +# - Copy the signing secret and update your config: +firebase functions:config:set stripe.webhook_secret="" +firebase deploy --only functions +``` + +## Security Checklist + +- [ ] **NEVER** commit real API keys or secrets to source control +- [ ] **ALWAYS** verify webhook signatures using `stripe.webhooks.constructEvent` +- [ ] Use test keys (starts with sk_test_) during development +- [ ] Use live keys (starts with sk_live_) only in production with proper security +- [ ] Enable Firebase Functions environment variables encryption +- [ ] Set up Firebase Security Rules for Firestore collections +- [ ] Implement proper IAM permissions for function deployment + +## Webhook Events + +The function handles the following Stripe webhook events: + +- `invoice.payment_succeeded` - Updates Firestore when payment succeeds +- `invoice.payment_failed` - Handles failed payments +- `customer.subscription.created` - Creates subscription record +- `customer.subscription.updated` - Updates subscription (tier changes, etc.) +- `customer.subscription.deleted` - Handles cancellations + +## Testing Webhooks Locally + +You can test webhooks locally using the Stripe CLI: + +```bash +# Install Stripe CLI +# https://stripe.com/docs/stripe-cli + +# Forward webhooks to local function +stripe listen --forward-to localhost:5001/PROJECT_ID/us-central1/stripeWebhook/webhook + +# Trigger test events +stripe trigger invoice.payment_succeeded +``` + +## Troubleshooting + +### Function Deployment Fails + +- Check that you have the correct Firebase project selected: `firebase use PROJECT_ID` +- Verify you have deployment permissions: `firebase projects:list` +- Check Node.js version compatibility in `functions/package.json` + +### Webhook Signature Verification Fails + +- Ensure you're using the correct webhook secret from the Stripe Dashboard +- Verify the secret matches the endpoint URL +- Check that you're using `bodyParser.raw()` and not parsing JSON before verification + +### Configuration Not Loading + +- Verify configuration: `firebase functions:config:get` +- After updating config, redeploy: `firebase deploy --only functions` +- Check Firebase logs: `firebase functions:log` + +## Additional Resources + +- [Firebase Cloud Functions Documentation](https://firebase.google.com/docs/functions) +- [Stripe Webhooks Guide](https://stripe.com/docs/webhooks) +- [Stripe Node.js Library](https://github.com/stripe/stripe-node) diff --git a/functions/stripe-webhooks/index.js b/functions/stripe-webhooks/index.js new file mode 100644 index 0000000..ab7032f --- /dev/null +++ b/functions/stripe-webhooks/index.js @@ -0,0 +1,192 @@ +/** + * Stripe Webhooks Handler for Firebase Cloud Functions + * + * This Express app receives and processes Stripe webhook events. + * It updates Firestore with subscription and payment information. + * + * SECURITY NOTES: + * - ALWAYS verify webhook signatures using stripe.webhooks.constructEvent + * - NEVER trust webhook data without signature verification + * - Use environment variables for secrets (NEVER commit real secrets) + * + * Environment Variables Required: + * - STRIPE_SECRET_KEY: Your Stripe secret key (sk_test_... or sk_live_...) + * - STRIPE_WEBHOOK_SECRET: Webhook signing secret from Stripe Dashboard (whsec_...) + */ + +const express = require('express'); +const bodyParser = require('body-parser'); + +// Initialize Stripe with secret key from environment +const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + +const app = express(); + +// IMPORTANT: Use raw body parser for Stripe webhook signature verification +// Stripe requires the raw request body to verify signatures +app.use(bodyParser.raw({ type: 'application/json' })); + +/** + * Stripe Webhook Endpoint + * + * Receives events from Stripe and updates Firestore accordingly + */ +app.post('/webhook', async (req, res) => { + const sig = req.headers['stripe-signature']; + const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; + + let event; + + try { + // TODO: Uncomment and implement signature verification + // This is CRITICAL for security - do not skip in production! + /* + event = stripe.webhooks.constructEvent( + req.body, + sig, + webhookSecret + ); + */ + + // TEMPORARY: Parse body directly (ONLY for development/testing) + // Remove this in production and use constructEvent above + event = JSON.parse(req.body.toString()); + + } catch (err) { + console.error('Webhook signature verification failed:', err.message); + return res.status(400).send(`Webhook Error: ${err.message}`); + } + + // Handle the event + console.log('Received webhook event:', event.type); + + try { + switch (event.type) { + case 'invoice.payment_succeeded': + await handleInvoicePaymentSucceeded(event.data.object); + break; + + case 'invoice.payment_failed': + await handleInvoicePaymentFailed(event.data.object); + break; + + case 'customer.subscription.created': + await handleSubscriptionCreated(event.data.object); + break; + + case 'customer.subscription.updated': + await handleSubscriptionUpdated(event.data.object); + break; + + case 'customer.subscription.deleted': + await handleSubscriptionDeleted(event.data.object); + break; + + default: + console.log(`Unhandled event type: ${event.type}`); + } + + // Return 200 to acknowledge receipt + res.json({ received: true }); + + } catch (error) { + console.error('Error processing webhook:', error); + res.status(500).json({ error: 'Webhook processing failed' }); + } +}); + +/** + * Handle successful invoice payment + * + * TODO: Implement Firestore update logic + * - Get firebaseUid from customer.metadata + * - Update /users/{uid}/subscriptions/{subId} with payment info + * - Update /entitlements/{uid} with tier and validUntil + */ +async function handleInvoicePaymentSucceeded(invoice) { + console.log('Invoice payment succeeded:', invoice.id); + + // TODO: Extract Firebase UID from customer metadata + // const customer = await stripe.customers.retrieve(invoice.customer); + // const firebaseUid = customer.metadata.firebaseUid; + + // TODO: Update Firestore + // const admin = require('firebase-admin'); + // const db = admin.firestore(); + // + // await db.collection('users').doc(firebaseUid) + // .collection('subscriptions').doc(invoice.subscription) + // .set({ + // status: 'active', + // lastPayment: new Date(invoice.created * 1000), + // amount: invoice.amount_paid, + // currency: invoice.currency, + // }, { merge: true }); + // + // // Update entitlements based on subscription tier + // await db.collection('entitlements').doc(firebaseUid).set({ + // tier: 'gold', // Determine from subscription product/price + // validUntil: new Date(invoice.period_end * 1000), + // updatedAt: new Date(), + // }); +} + +/** + * Handle failed invoice payment + * + * TODO: Implement Firestore update and user notification + */ +async function handleInvoicePaymentFailed(invoice) { + console.log('Invoice payment failed:', invoice.id); + + // TODO: Update subscription status to 'payment_failed' + // TODO: Optionally downgrade entitlements or show grace period + // TODO: Send notification to user about failed payment +} + +/** + * Handle subscription created + * + * TODO: Create subscription record in Firestore + */ +async function handleSubscriptionCreated(subscription) { + console.log('Subscription created:', subscription.id); + + // TODO: Extract Firebase UID from customer.metadata + // TODO: Create /users/{uid}/subscriptions/{subId} document + // TODO: Set initial entitlements based on subscription tier +} + +/** + * Handle subscription updated + * + * TODO: Update subscription record and entitlements + */ +async function handleSubscriptionUpdated(subscription) { + console.log('Subscription updated:', subscription.id); + + // TODO: Update subscription status, tier changes, etc. + // TODO: Handle tier upgrades/downgrades +} + +/** + * Handle subscription deleted/cancelled + * + * TODO: Update subscription record and revoke entitlements + */ +async function handleSubscriptionDeleted(subscription) { + console.log('Subscription deleted:', subscription.id); + + // TODO: Mark subscription as cancelled + // TODO: Update entitlements (either immediate or at period_end) + // TODO: Consider grace period handling +} + +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ status: 'ok', service: 'stripe-webhooks' }); +}); + +// Export the Express app for Firebase Cloud Functions +// Usage in Firebase: exports.stripeWebhook = functions.https.onRequest(app); +module.exports = app; diff --git a/src/components/ChannelLock.stubs.jsx b/src/components/ChannelLock.stubs.jsx new file mode 100644 index 0000000..208fd9d --- /dev/null +++ b/src/components/ChannelLock.stubs.jsx @@ -0,0 +1,152 @@ +/** + * ChannelLock.stubs.jsx + * + * React component demonstrating client-side gating for premium channels. + * This is a STUB implementation using mock data for demonstration purposes. + * + * TODO: Replace mock useUserEntitlements with real Firestore-backed entitlement checks + * TODO: Connect "Upgrade to Gold" CTA to actual Stripe Checkout flow + */ + +import React from 'react'; + +/** + * Mock hook for user entitlements + * + * TODO: Replace this with a real hook that: + * 1. Reads from Firestore /entitlements/{uid} collection + * 2. Subscribes to real-time updates when subscription changes + * 3. Handles authentication state (returns null tier when not logged in) + * + * Example real implementation: + * const useUserEntitlements = () => { + * const [entitlement, setEntitlement] = useState(null); + * const { user } = useAuth(); + * + * useEffect(() => { + * if (!user) return; + * const unsubscribe = firestore + * .collection('entitlements') + * .doc(user.uid) + * .onSnapshot(doc => setEntitlement(doc.data())); + * return unsubscribe; + * }, [user]); + * + * return entitlement; + * }; + */ +const useUserEntitlements = () => { + // Mock entitlement - replace with real Firestore query + return { + tier: 'silver', // Mock: user has silver tier + validUntil: new Date('2025-12-31'), + }; +}; + +/** + * ChannelLock component + * + * Renders a lock overlay for channels that require a higher tier subscription + * + * @param {Object} props + * @param {Object} props.channel - Channel object with tier property + * @param {React.ReactNode} props.children - Channel content to potentially lock + */ +const ChannelLock = ({ channel, children }) => { + const userEntitlement = useUserEntitlements(); + + // Determine if user has access to this channel + const isLocked = channel.tier === 'gold' && userEntitlement?.tier !== 'gold'; + + if (!isLocked) { + // User has access, render channel content normally + return <>{children}; + } + + // User doesn't have access, show lock overlay + return ( +
+ {/* Blurred/dimmed background content */} +
+ {children} +
+ + {/* Lock overlay */} +
+ {/* Lock icon */} +
+ 🔒 +
+ + {/* Message */} +

+ Premium Channel +

+

+ This channel requires a Gold tier subscription. + Upgrade now to unlock this and other premium channels. +

+ + {/* Upgrade CTA */} + +
+
+ ); +}; + +export default ChannelLock; diff --git a/src/data/channels.full.json b/src/data/channels.full.json new file mode 100644 index 0000000..89601cd --- /dev/null +++ b/src/data/channels.full.json @@ -0,0 +1,2162 @@ +[ + { + "id": 25, + "name": "Channel 25", + "country": "MX", + "language": "es", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel25.png", + "stream": "https://stream.placeholder.example.com/live/channel25/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel25.xml", + "type": "vod" + }, + { + "id": 26, + "name": "Channel 26", + "country": "IN", + "language": "it", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel26.png", + "stream": "https://stream.placeholder.example.com/live/channel26/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel26.xml", + "type": "catchup", + "tier": "gold" + }, + { + "id": 27, + "name": "Channel 27", + "country": "NL", + "language": "ja", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel27.png", + "stream": "https://stream.placeholder.example.com/live/channel27/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel27.xml", + "type": "live" + }, + { + "id": 28, + "name": "Channel 28", + "country": "SE", + "language": "pt", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel28.png", + "stream": "https://stream.placeholder.example.com/live/channel28/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel28.xml", + "type": "vod" + }, + { + "id": 29, + "name": "Channel 29", + "country": "NO", + "language": "hi", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel29.png", + "stream": "https://stream.placeholder.example.com/live/channel29/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel29.xml", + "type": "catchup" + }, + { + "id": 30, + "name": "Channel 30", + "country": "US", + "language": "nl", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel30.png", + "stream": "https://stream.placeholder.example.com/live/channel30/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel30.xml", + "type": "live" + }, + { + "id": 31, + "name": "Channel 31", + "country": "UK", + "language": "sv", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel31.png", + "stream": "https://stream.placeholder.example.com/live/channel31/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel31.xml", + "type": "vod" + }, + { + "id": 32, + "name": "Channel 32", + "country": "CA", + "language": "no", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel32.png", + "stream": "https://stream.placeholder.example.com/live/channel32/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel32.xml", + "type": "catchup" + }, + { + "id": 33, + "name": "Channel 33", + "country": "AU", + "language": "en", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel33.png", + "stream": "https://stream.placeholder.example.com/live/channel33/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel33.xml", + "type": "live", + "tier": "gold" + }, + { + "id": 34, + "name": "Channel 34", + "country": "DE", + "language": "de", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel34.png", + "stream": "https://stream.placeholder.example.com/live/channel34/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel34.xml", + "type": "vod" + }, + { + "id": 35, + "name": "Channel 35", + "country": "FR", + "language": "fr", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel35.png", + "stream": "https://stream.placeholder.example.com/live/channel35/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel35.xml", + "type": "catchup" + }, + { + "id": 36, + "name": "Channel 36", + "country": "ES", + "language": "es", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel36.png", + "stream": "https://stream.placeholder.example.com/live/channel36/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel36.xml", + "type": "live", + "tier": "gold" + }, + { + "id": 37, + "name": "Channel 37", + "country": "IT", + "language": "it", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel37.png", + "stream": "https://stream.placeholder.example.com/live/channel37/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel37.xml", + "type": "vod" + }, + { + "id": 38, + "name": "Channel 38", + "country": "JP", + "language": "ja", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel38.png", + "stream": "https://stream.placeholder.example.com/live/channel38/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel38.xml", + "type": "catchup" + }, + { + "id": 39, + "name": "Channel 39", + "country": "BR", + "language": "pt", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel39.png", + "stream": "https://stream.placeholder.example.com/live/channel39/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel39.xml", + "type": "live" + }, + { + "id": 40, + "name": "Channel 40", + "country": "MX", + "language": "hi", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel40.png", + "stream": "https://stream.placeholder.example.com/live/channel40/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel40.xml", + "type": "vod", + "tier": "gold" + }, + { + "id": 41, + "name": "Channel 41", + "country": "IN", + "language": "nl", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel41.png", + "stream": "https://stream.placeholder.example.com/live/channel41/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel41.xml", + "type": "catchup" + }, + { + "id": 42, + "name": "Channel 42", + "country": "NL", + "language": "sv", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel42.png", + "stream": "https://stream.placeholder.example.com/live/channel42/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel42.xml", + "type": "live" + }, + { + "id": 43, + "name": "Channel 43", + "country": "SE", + "language": "no", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel43.png", + "stream": "https://stream.placeholder.example.com/live/channel43/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel43.xml", + "type": "vod" + }, + { + "id": 44, + "name": "Channel 44", + "country": "NO", + "language": "en", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel44.png", + "stream": "https://stream.placeholder.example.com/live/channel44/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel44.xml", + "type": "catchup" + }, + { + "id": 45, + "name": "Channel 45", + "country": "US", + "language": "de", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel45.png", + "stream": "https://stream.placeholder.example.com/live/channel45/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel45.xml", + "type": "live" + }, + { + "id": 46, + "name": "Channel 46", + "country": "UK", + "language": "fr", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel46.png", + "stream": "https://stream.placeholder.example.com/live/channel46/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel46.xml", + "type": "vod" + }, + { + "id": 47, + "name": "Channel 47", + "country": "CA", + "language": "es", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel47.png", + "stream": "https://stream.placeholder.example.com/live/channel47/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel47.xml", + "type": "catchup" + }, + { + "id": 48, + "name": "Channel 48", + "country": "AU", + "language": "it", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel48.png", + "stream": "https://stream.placeholder.example.com/live/channel48/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel48.xml", + "type": "live" + }, + { + "id": 49, + "name": "Channel 49", + "country": "DE", + "language": "ja", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel49.png", + "stream": "https://stream.placeholder.example.com/live/channel49/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel49.xml", + "type": "vod" + }, + { + "id": 50, + "name": "Channel 50", + "country": "FR", + "language": "pt", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel50.png", + "stream": "https://stream.placeholder.example.com/live/channel50/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel50.xml", + "type": "catchup" + }, + { + "id": 51, + "name": "Channel 51", + "country": "ES", + "language": "hi", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel51.png", + "stream": "https://stream.placeholder.example.com/live/channel51/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel51.xml", + "type": "live" + }, + { + "id": 52, + "name": "Channel 52", + "country": "IT", + "language": "nl", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel52.png", + "stream": "https://stream.placeholder.example.com/live/channel52/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel52.xml", + "type": "vod" + }, + { + "id": 53, + "name": "Channel 53", + "country": "JP", + "language": "sv", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel53.png", + "stream": "https://stream.placeholder.example.com/live/channel53/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel53.xml", + "type": "catchup" + }, + { + "id": 54, + "name": "Channel 54", + "country": "BR", + "language": "no", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel54.png", + "stream": "https://stream.placeholder.example.com/live/channel54/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel54.xml", + "type": "live" + }, + { + "id": 55, + "name": "Channel 55", + "country": "MX", + "language": "en", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel55.png", + "stream": "https://stream.placeholder.example.com/live/channel55/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel55.xml", + "type": "vod" + }, + { + "id": 56, + "name": "Channel 56", + "country": "IN", + "language": "de", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel56.png", + "stream": "https://stream.placeholder.example.com/live/channel56/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel56.xml", + "type": "catchup" + }, + { + "id": 57, + "name": "Channel 57", + "country": "NL", + "language": "fr", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel57.png", + "stream": "https://stream.placeholder.example.com/live/channel57/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel57.xml", + "type": "live" + }, + { + "id": 58, + "name": "Channel 58", + "country": "SE", + "language": "es", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel58.png", + "stream": "https://stream.placeholder.example.com/live/channel58/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel58.xml", + "type": "vod" + }, + { + "id": 59, + "name": "Channel 59", + "country": "NO", + "language": "it", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel59.png", + "stream": "https://stream.placeholder.example.com/live/channel59/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel59.xml", + "type": "catchup" + }, + { + "id": 60, + "name": "Channel 60", + "country": "US", + "language": "ja", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel60.png", + "stream": "https://stream.placeholder.example.com/live/channel60/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel60.xml", + "type": "live" + }, + { + "id": 61, + "name": "Channel 61", + "country": "UK", + "language": "pt", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel61.png", + "stream": "https://stream.placeholder.example.com/live/channel61/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel61.xml", + "type": "vod" + }, + { + "id": 62, + "name": "Channel 62", + "country": "CA", + "language": "hi", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel62.png", + "stream": "https://stream.placeholder.example.com/live/channel62/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel62.xml", + "type": "catchup" + }, + { + "id": 63, + "name": "Channel 63", + "country": "AU", + "language": "nl", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel63.png", + "stream": "https://stream.placeholder.example.com/live/channel63/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel63.xml", + "type": "live" + }, + { + "id": 64, + "name": "Channel 64", + "country": "DE", + "language": "sv", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel64.png", + "stream": "https://stream.placeholder.example.com/live/channel64/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel64.xml", + "type": "vod" + }, + { + "id": 65, + "name": "Channel 65", + "country": "FR", + "language": "no", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel65.png", + "stream": "https://stream.placeholder.example.com/live/channel65/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel65.xml", + "type": "catchup" + }, + { + "id": 66, + "name": "Channel 66", + "country": "ES", + "language": "en", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel66.png", + "stream": "https://stream.placeholder.example.com/live/channel66/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel66.xml", + "type": "live" + }, + { + "id": 67, + "name": "Channel 67", + "country": "IT", + "language": "de", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel67.png", + "stream": "https://stream.placeholder.example.com/live/channel67/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel67.xml", + "type": "vod" + }, + { + "id": 68, + "name": "Channel 68", + "country": "JP", + "language": "fr", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel68.png", + "stream": "https://stream.placeholder.example.com/live/channel68/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel68.xml", + "type": "catchup" + }, + { + "id": 69, + "name": "Channel 69", + "country": "BR", + "language": "es", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel69.png", + "stream": "https://stream.placeholder.example.com/live/channel69/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel69.xml", + "type": "live" + }, + { + "id": 70, + "name": "Channel 70", + "country": "MX", + "language": "it", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel70.png", + "stream": "https://stream.placeholder.example.com/live/channel70/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel70.xml", + "type": "vod" + }, + { + "id": 71, + "name": "Channel 71", + "country": "IN", + "language": "ja", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel71.png", + "stream": "https://stream.placeholder.example.com/live/channel71/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel71.xml", + "type": "catchup" + }, + { + "id": 72, + "name": "Channel 72", + "country": "NL", + "language": "pt", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel72.png", + "stream": "https://stream.placeholder.example.com/live/channel72/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel72.xml", + "type": "live" + }, + { + "id": 73, + "name": "Channel 73", + "country": "SE", + "language": "hi", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel73.png", + "stream": "https://stream.placeholder.example.com/live/channel73/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel73.xml", + "type": "vod" + }, + { + "id": 74, + "name": "Channel 74", + "country": "NO", + "language": "nl", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel74.png", + "stream": "https://stream.placeholder.example.com/live/channel74/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel74.xml", + "type": "catchup" + }, + { + "id": 75, + "name": "Channel 75", + "country": "US", + "language": "sv", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel75.png", + "stream": "https://stream.placeholder.example.com/live/channel75/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel75.xml", + "type": "live" + }, + { + "id": 76, + "name": "Channel 76", + "country": "UK", + "language": "no", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel76.png", + "stream": "https://stream.placeholder.example.com/live/channel76/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel76.xml", + "type": "vod" + }, + { + "id": 77, + "name": "Channel 77", + "country": "CA", + "language": "en", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel77.png", + "stream": "https://stream.placeholder.example.com/live/channel77/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel77.xml", + "type": "catchup" + }, + { + "id": 78, + "name": "Channel 78", + "country": "AU", + "language": "de", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel78.png", + "stream": "https://stream.placeholder.example.com/live/channel78/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel78.xml", + "type": "live" + }, + { + "id": 79, + "name": "Channel 79", + "country": "DE", + "language": "fr", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel79.png", + "stream": "https://stream.placeholder.example.com/live/channel79/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel79.xml", + "type": "vod" + }, + { + "id": 80, + "name": "Channel 80", + "country": "FR", + "language": "es", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel80.png", + "stream": "https://stream.placeholder.example.com/live/channel80/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel80.xml", + "type": "catchup" + }, + { + "id": 81, + "name": "Channel 81", + "country": "ES", + "language": "it", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel81.png", + "stream": "https://stream.placeholder.example.com/live/channel81/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel81.xml", + "type": "live" + }, + { + "id": 82, + "name": "Channel 82", + "country": "IT", + "language": "ja", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel82.png", + "stream": "https://stream.placeholder.example.com/live/channel82/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel82.xml", + "type": "vod" + }, + { + "id": 83, + "name": "Channel 83", + "country": "JP", + "language": "pt", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel83.png", + "stream": "https://stream.placeholder.example.com/live/channel83/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel83.xml", + "type": "catchup" + }, + { + "id": 84, + "name": "Channel 84", + "country": "BR", + "language": "hi", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel84.png", + "stream": "https://stream.placeholder.example.com/live/channel84/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel84.xml", + "type": "live" + }, + { + "id": 85, + "name": "Channel 85", + "country": "MX", + "language": "nl", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel85.png", + "stream": "https://stream.placeholder.example.com/live/channel85/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel85.xml", + "type": "vod" + }, + { + "id": 86, + "name": "Channel 86", + "country": "IN", + "language": "sv", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel86.png", + "stream": "https://stream.placeholder.example.com/live/channel86/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel86.xml", + "type": "catchup" + }, + { + "id": 87, + "name": "Channel 87", + "country": "NL", + "language": "no", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel87.png", + "stream": "https://stream.placeholder.example.com/live/channel87/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel87.xml", + "type": "live" + }, + { + "id": 88, + "name": "Channel 88", + "country": "SE", + "language": "en", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel88.png", + "stream": "https://stream.placeholder.example.com/live/channel88/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel88.xml", + "type": "vod" + }, + { + "id": 89, + "name": "Channel 89", + "country": "NO", + "language": "de", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel89.png", + "stream": "https://stream.placeholder.example.com/live/channel89/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel89.xml", + "type": "catchup" + }, + { + "id": 90, + "name": "Channel 90", + "country": "US", + "language": "fr", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel90.png", + "stream": "https://stream.placeholder.example.com/live/channel90/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel90.xml", + "type": "live" + }, + { + "id": 91, + "name": "Channel 91", + "country": "UK", + "language": "es", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel91.png", + "stream": "https://stream.placeholder.example.com/live/channel91/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel91.xml", + "type": "vod" + }, + { + "id": 92, + "name": "Channel 92", + "country": "CA", + "language": "it", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel92.png", + "stream": "https://stream.placeholder.example.com/live/channel92/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel92.xml", + "type": "catchup" + }, + { + "id": 93, + "name": "Channel 93", + "country": "AU", + "language": "ja", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel93.png", + "stream": "https://stream.placeholder.example.com/live/channel93/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel93.xml", + "type": "live" + }, + { + "id": 94, + "name": "Channel 94", + "country": "DE", + "language": "pt", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel94.png", + "stream": "https://stream.placeholder.example.com/live/channel94/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel94.xml", + "type": "vod" + }, + { + "id": 95, + "name": "Channel 95", + "country": "FR", + "language": "hi", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel95.png", + "stream": "https://stream.placeholder.example.com/live/channel95/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel95.xml", + "type": "catchup" + }, + { + "id": 96, + "name": "Channel 96", + "country": "ES", + "language": "nl", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel96.png", + "stream": "https://stream.placeholder.example.com/live/channel96/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel96.xml", + "type": "live" + }, + { + "id": 97, + "name": "Channel 97", + "country": "IT", + "language": "sv", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel97.png", + "stream": "https://stream.placeholder.example.com/live/channel97/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel97.xml", + "type": "vod" + }, + { + "id": 98, + "name": "Channel 98", + "country": "JP", + "language": "no", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel98.png", + "stream": "https://stream.placeholder.example.com/live/channel98/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel98.xml", + "type": "catchup" + }, + { + "id": 99, + "name": "Channel 99", + "country": "BR", + "language": "en", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel99.png", + "stream": "https://stream.placeholder.example.com/live/channel99/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel99.xml", + "type": "live" + }, + { + "id": 100, + "name": "Channel 100", + "country": "MX", + "language": "de", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel100.png", + "stream": "https://stream.placeholder.example.com/live/channel100/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel100.xml", + "type": "vod" + }, + { + "id": 101, + "name": "Channel 101", + "country": "IN", + "language": "fr", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel101.png", + "stream": "https://stream.placeholder.example.com/live/channel101/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel101.xml", + "type": "catchup" + }, + { + "id": 102, + "name": "Channel 102", + "country": "NL", + "language": "es", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel102.png", + "stream": "https://stream.placeholder.example.com/live/channel102/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel102.xml", + "type": "live" + }, + { + "id": 103, + "name": "Channel 103", + "country": "SE", + "language": "it", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel103.png", + "stream": "https://stream.placeholder.example.com/live/channel103/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel103.xml", + "type": "vod" + }, + { + "id": 104, + "name": "Channel 104", + "country": "NO", + "language": "ja", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel104.png", + "stream": "https://stream.placeholder.example.com/live/channel104/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel104.xml", + "type": "catchup" + }, + { + "id": 105, + "name": "Channel 105", + "country": "US", + "language": "pt", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel105.png", + "stream": "https://stream.placeholder.example.com/live/channel105/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel105.xml", + "type": "live" + }, + { + "id": 106, + "name": "Channel 106", + "country": "UK", + "language": "hi", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel106.png", + "stream": "https://stream.placeholder.example.com/live/channel106/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel106.xml", + "type": "vod" + }, + { + "id": 107, + "name": "Channel 107", + "country": "CA", + "language": "nl", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel107.png", + "stream": "https://stream.placeholder.example.com/live/channel107/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel107.xml", + "type": "catchup" + }, + { + "id": 108, + "name": "Channel 108", + "country": "AU", + "language": "sv", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel108.png", + "stream": "https://stream.placeholder.example.com/live/channel108/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel108.xml", + "type": "live" + }, + { + "id": 109, + "name": "Channel 109", + "country": "DE", + "language": "no", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel109.png", + "stream": "https://stream.placeholder.example.com/live/channel109/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel109.xml", + "type": "vod" + }, + { + "id": 110, + "name": "Channel 110", + "country": "FR", + "language": "en", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel110.png", + "stream": "https://stream.placeholder.example.com/live/channel110/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel110.xml", + "type": "catchup" + }, + { + "id": 111, + "name": "Channel 111", + "country": "ES", + "language": "de", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel111.png", + "stream": "https://stream.placeholder.example.com/live/channel111/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel111.xml", + "type": "live" + }, + { + "id": 112, + "name": "Channel 112", + "country": "IT", + "language": "fr", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel112.png", + "stream": "https://stream.placeholder.example.com/live/channel112/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel112.xml", + "type": "vod" + }, + { + "id": 113, + "name": "Channel 113", + "country": "JP", + "language": "es", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel113.png", + "stream": "https://stream.placeholder.example.com/live/channel113/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel113.xml", + "type": "catchup" + }, + { + "id": 114, + "name": "Channel 114", + "country": "BR", + "language": "it", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel114.png", + "stream": "https://stream.placeholder.example.com/live/channel114/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel114.xml", + "type": "live" + }, + { + "id": 115, + "name": "Channel 115", + "country": "MX", + "language": "ja", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel115.png", + "stream": "https://stream.placeholder.example.com/live/channel115/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel115.xml", + "type": "vod" + }, + { + "id": 116, + "name": "Channel 116", + "country": "IN", + "language": "pt", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel116.png", + "stream": "https://stream.placeholder.example.com/live/channel116/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel116.xml", + "type": "catchup" + }, + { + "id": 117, + "name": "Channel 117", + "country": "NL", + "language": "hi", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel117.png", + "stream": "https://stream.placeholder.example.com/live/channel117/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel117.xml", + "type": "live" + }, + { + "id": 118, + "name": "Channel 118", + "country": "SE", + "language": "nl", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel118.png", + "stream": "https://stream.placeholder.example.com/live/channel118/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel118.xml", + "type": "vod" + }, + { + "id": 119, + "name": "Channel 119", + "country": "NO", + "language": "sv", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel119.png", + "stream": "https://stream.placeholder.example.com/live/channel119/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel119.xml", + "type": "catchup" + }, + { + "id": 120, + "name": "Channel 120", + "country": "US", + "language": "no", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel120.png", + "stream": "https://stream.placeholder.example.com/live/channel120/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel120.xml", + "type": "live" + }, + { + "id": 121, + "name": "Channel 121", + "country": "UK", + "language": "en", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel121.png", + "stream": "https://stream.placeholder.example.com/live/channel121/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel121.xml", + "type": "vod" + }, + { + "id": 122, + "name": "Channel 122", + "country": "CA", + "language": "de", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel122.png", + "stream": "https://stream.placeholder.example.com/live/channel122/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel122.xml", + "type": "catchup" + }, + { + "id": 123, + "name": "Channel 123", + "country": "AU", + "language": "fr", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel123.png", + "stream": "https://stream.placeholder.example.com/live/channel123/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel123.xml", + "type": "live" + }, + { + "id": 124, + "name": "Channel 124", + "country": "DE", + "language": "es", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel124.png", + "stream": "https://stream.placeholder.example.com/live/channel124/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel124.xml", + "type": "vod" + }, + { + "id": 125, + "name": "Channel 125", + "country": "FR", + "language": "it", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel125.png", + "stream": "https://stream.placeholder.example.com/live/channel125/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel125.xml", + "type": "catchup" + }, + { + "id": 126, + "name": "Channel 126", + "country": "ES", + "language": "ja", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel126.png", + "stream": "https://stream.placeholder.example.com/live/channel126/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel126.xml", + "type": "live" + }, + { + "id": 127, + "name": "Channel 127", + "country": "IT", + "language": "pt", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel127.png", + "stream": "https://stream.placeholder.example.com/live/channel127/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel127.xml", + "type": "vod" + }, + { + "id": 128, + "name": "Channel 128", + "country": "JP", + "language": "hi", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel128.png", + "stream": "https://stream.placeholder.example.com/live/channel128/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel128.xml", + "type": "catchup" + }, + { + "id": 129, + "name": "Channel 129", + "country": "BR", + "language": "nl", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel129.png", + "stream": "https://stream.placeholder.example.com/live/channel129/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel129.xml", + "type": "live" + }, + { + "id": 130, + "name": "Channel 130", + "country": "MX", + "language": "sv", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel130.png", + "stream": "https://stream.placeholder.example.com/live/channel130/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel130.xml", + "type": "vod" + }, + { + "id": 131, + "name": "Channel 131", + "country": "IN", + "language": "no", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel131.png", + "stream": "https://stream.placeholder.example.com/live/channel131/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel131.xml", + "type": "catchup" + }, + { + "id": 132, + "name": "Channel 132", + "country": "NL", + "language": "en", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel132.png", + "stream": "https://stream.placeholder.example.com/live/channel132/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel132.xml", + "type": "live" + }, + { + "id": 133, + "name": "Channel 133", + "country": "SE", + "language": "de", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel133.png", + "stream": "https://stream.placeholder.example.com/live/channel133/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel133.xml", + "type": "vod" + }, + { + "id": 134, + "name": "Channel 134", + "country": "NO", + "language": "fr", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel134.png", + "stream": "https://stream.placeholder.example.com/live/channel134/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel134.xml", + "type": "catchup" + }, + { + "id": 135, + "name": "Channel 135", + "country": "US", + "language": "es", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel135.png", + "stream": "https://stream.placeholder.example.com/live/channel135/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel135.xml", + "type": "live" + }, + { + "id": 136, + "name": "Channel 136", + "country": "UK", + "language": "it", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel136.png", + "stream": "https://stream.placeholder.example.com/live/channel136/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel136.xml", + "type": "vod" + }, + { + "id": 137, + "name": "Channel 137", + "country": "CA", + "language": "ja", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel137.png", + "stream": "https://stream.placeholder.example.com/live/channel137/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel137.xml", + "type": "catchup" + }, + { + "id": 138, + "name": "Channel 138", + "country": "AU", + "language": "pt", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel138.png", + "stream": "https://stream.placeholder.example.com/live/channel138/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel138.xml", + "type": "live" + }, + { + "id": 139, + "name": "Channel 139", + "country": "DE", + "language": "hi", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel139.png", + "stream": "https://stream.placeholder.example.com/live/channel139/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel139.xml", + "type": "vod" + }, + { + "id": 140, + "name": "Channel 140", + "country": "FR", + "language": "nl", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel140.png", + "stream": "https://stream.placeholder.example.com/live/channel140/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel140.xml", + "type": "catchup" + }, + { + "id": 141, + "name": "Channel 141", + "country": "ES", + "language": "sv", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel141.png", + "stream": "https://stream.placeholder.example.com/live/channel141/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel141.xml", + "type": "live" + }, + { + "id": 142, + "name": "Channel 142", + "country": "IT", + "language": "no", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel142.png", + "stream": "https://stream.placeholder.example.com/live/channel142/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel142.xml", + "type": "vod" + }, + { + "id": 143, + "name": "Channel 143", + "country": "JP", + "language": "en", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel143.png", + "stream": "https://stream.placeholder.example.com/live/channel143/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel143.xml", + "type": "catchup" + }, + { + "id": 144, + "name": "Channel 144", + "country": "BR", + "language": "de", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel144.png", + "stream": "https://stream.placeholder.example.com/live/channel144/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel144.xml", + "type": "live" + }, + { + "id": 145, + "name": "Channel 145", + "country": "MX", + "language": "fr", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel145.png", + "stream": "https://stream.placeholder.example.com/live/channel145/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel145.xml", + "type": "vod" + }, + { + "id": 146, + "name": "Channel 146", + "country": "IN", + "language": "es", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel146.png", + "stream": "https://stream.placeholder.example.com/live/channel146/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel146.xml", + "type": "catchup" + }, + { + "id": 147, + "name": "Channel 147", + "country": "NL", + "language": "it", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel147.png", + "stream": "https://stream.placeholder.example.com/live/channel147/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel147.xml", + "type": "live" + }, + { + "id": 148, + "name": "Channel 148", + "country": "SE", + "language": "ja", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel148.png", + "stream": "https://stream.placeholder.example.com/live/channel148/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel148.xml", + "type": "vod" + }, + { + "id": 149, + "name": "Channel 149", + "country": "NO", + "language": "pt", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel149.png", + "stream": "https://stream.placeholder.example.com/live/channel149/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel149.xml", + "type": "catchup" + }, + { + "id": 150, + "name": "Channel 150", + "country": "US", + "language": "hi", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel150.png", + "stream": "https://stream.placeholder.example.com/live/channel150/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel150.xml", + "type": "live" + }, + { + "id": 151, + "name": "Channel 151", + "country": "UK", + "language": "nl", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel151.png", + "stream": "https://stream.placeholder.example.com/live/channel151/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel151.xml", + "type": "vod" + }, + { + "id": 152, + "name": "Channel 152", + "country": "CA", + "language": "sv", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel152.png", + "stream": "https://stream.placeholder.example.com/live/channel152/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel152.xml", + "type": "catchup" + }, + { + "id": 153, + "name": "Channel 153", + "country": "AU", + "language": "no", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel153.png", + "stream": "https://stream.placeholder.example.com/live/channel153/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel153.xml", + "type": "live" + }, + { + "id": 154, + "name": "Channel 154", + "country": "DE", + "language": "en", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel154.png", + "stream": "https://stream.placeholder.example.com/live/channel154/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel154.xml", + "type": "vod" + }, + { + "id": 155, + "name": "Channel 155", + "country": "FR", + "language": "de", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel155.png", + "stream": "https://stream.placeholder.example.com/live/channel155/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel155.xml", + "type": "catchup" + }, + { + "id": 156, + "name": "Channel 156", + "country": "ES", + "language": "fr", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel156.png", + "stream": "https://stream.placeholder.example.com/live/channel156/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel156.xml", + "type": "live" + }, + { + "id": 157, + "name": "Channel 157", + "country": "IT", + "language": "es", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel157.png", + "stream": "https://stream.placeholder.example.com/live/channel157/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel157.xml", + "type": "vod" + }, + { + "id": 158, + "name": "Channel 158", + "country": "JP", + "language": "it", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel158.png", + "stream": "https://stream.placeholder.example.com/live/channel158/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel158.xml", + "type": "catchup" + }, + { + "id": 159, + "name": "Channel 159", + "country": "BR", + "language": "ja", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel159.png", + "stream": "https://stream.placeholder.example.com/live/channel159/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel159.xml", + "type": "live" + }, + { + "id": 160, + "name": "Channel 160", + "country": "MX", + "language": "pt", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel160.png", + "stream": "https://stream.placeholder.example.com/live/channel160/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel160.xml", + "type": "vod" + }, + { + "id": 161, + "name": "Channel 161", + "country": "IN", + "language": "hi", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel161.png", + "stream": "https://stream.placeholder.example.com/live/channel161/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel161.xml", + "type": "catchup" + }, + { + "id": 162, + "name": "Channel 162", + "country": "NL", + "language": "nl", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel162.png", + "stream": "https://stream.placeholder.example.com/live/channel162/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel162.xml", + "type": "live" + }, + { + "id": 163, + "name": "Channel 163", + "country": "SE", + "language": "sv", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel163.png", + "stream": "https://stream.placeholder.example.com/live/channel163/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel163.xml", + "type": "vod" + }, + { + "id": 164, + "name": "Channel 164", + "country": "NO", + "language": "no", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel164.png", + "stream": "https://stream.placeholder.example.com/live/channel164/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel164.xml", + "type": "catchup" + }, + { + "id": 165, + "name": "Channel 165", + "country": "US", + "language": "en", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel165.png", + "stream": "https://stream.placeholder.example.com/live/channel165/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel165.xml", + "type": "live" + }, + { + "id": 166, + "name": "Channel 166", + "country": "UK", + "language": "de", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel166.png", + "stream": "https://stream.placeholder.example.com/live/channel166/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel166.xml", + "type": "vod" + }, + { + "id": 167, + "name": "Channel 167", + "country": "CA", + "language": "fr", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel167.png", + "stream": "https://stream.placeholder.example.com/live/channel167/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel167.xml", + "type": "catchup" + }, + { + "id": 168, + "name": "Channel 168", + "country": "AU", + "language": "es", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel168.png", + "stream": "https://stream.placeholder.example.com/live/channel168/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel168.xml", + "type": "live" + }, + { + "id": 169, + "name": "Channel 169", + "country": "DE", + "language": "it", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel169.png", + "stream": "https://stream.placeholder.example.com/live/channel169/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel169.xml", + "type": "vod" + }, + { + "id": 170, + "name": "Channel 170", + "country": "FR", + "language": "ja", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel170.png", + "stream": "https://stream.placeholder.example.com/live/channel170/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel170.xml", + "type": "catchup" + }, + { + "id": 171, + "name": "Channel 171", + "country": "ES", + "language": "pt", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel171.png", + "stream": "https://stream.placeholder.example.com/live/channel171/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel171.xml", + "type": "live" + }, + { + "id": 172, + "name": "Channel 172", + "country": "IT", + "language": "hi", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel172.png", + "stream": "https://stream.placeholder.example.com/live/channel172/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel172.xml", + "type": "vod" + }, + { + "id": 173, + "name": "Channel 173", + "country": "JP", + "language": "nl", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel173.png", + "stream": "https://stream.placeholder.example.com/live/channel173/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel173.xml", + "type": "catchup" + }, + { + "id": 174, + "name": "Channel 174", + "country": "BR", + "language": "sv", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel174.png", + "stream": "https://stream.placeholder.example.com/live/channel174/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel174.xml", + "type": "live" + }, + { + "id": 175, + "name": "Channel 175", + "country": "MX", + "language": "no", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel175.png", + "stream": "https://stream.placeholder.example.com/live/channel175/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel175.xml", + "type": "vod" + }, + { + "id": 176, + "name": "Channel 176", + "country": "IN", + "language": "en", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel176.png", + "stream": "https://stream.placeholder.example.com/live/channel176/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel176.xml", + "type": "catchup" + }, + { + "id": 177, + "name": "Channel 177", + "country": "NL", + "language": "de", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel177.png", + "stream": "https://stream.placeholder.example.com/live/channel177/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel177.xml", + "type": "live" + }, + { + "id": 178, + "name": "Channel 178", + "country": "SE", + "language": "fr", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel178.png", + "stream": "https://stream.placeholder.example.com/live/channel178/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel178.xml", + "type": "vod" + }, + { + "id": 179, + "name": "Channel 179", + "country": "NO", + "language": "es", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel179.png", + "stream": "https://stream.placeholder.example.com/live/channel179/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel179.xml", + "type": "catchup" + }, + { + "id": 180, + "name": "Channel 180", + "country": "US", + "language": "it", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel180.png", + "stream": "https://stream.placeholder.example.com/live/channel180/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel180.xml", + "type": "live" + }, + { + "id": 181, + "name": "Channel 181", + "country": "UK", + "language": "ja", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel181.png", + "stream": "https://stream.placeholder.example.com/live/channel181/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel181.xml", + "type": "vod" + }, + { + "id": 182, + "name": "Channel 182", + "country": "CA", + "language": "pt", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel182.png", + "stream": "https://stream.placeholder.example.com/live/channel182/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel182.xml", + "type": "catchup" + }, + { + "id": 183, + "name": "Channel 183", + "country": "AU", + "language": "hi", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel183.png", + "stream": "https://stream.placeholder.example.com/live/channel183/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel183.xml", + "type": "live" + }, + { + "id": 184, + "name": "Channel 184", + "country": "DE", + "language": "nl", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel184.png", + "stream": "https://stream.placeholder.example.com/live/channel184/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel184.xml", + "type": "vod" + }, + { + "id": 185, + "name": "Channel 185", + "country": "FR", + "language": "sv", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel185.png", + "stream": "https://stream.placeholder.example.com/live/channel185/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel185.xml", + "type": "catchup" + }, + { + "id": 186, + "name": "Channel 186", + "country": "ES", + "language": "no", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel186.png", + "stream": "https://stream.placeholder.example.com/live/channel186/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel186.xml", + "type": "live" + }, + { + "id": 187, + "name": "Channel 187", + "country": "IT", + "language": "en", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel187.png", + "stream": "https://stream.placeholder.example.com/live/channel187/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel187.xml", + "type": "vod" + }, + { + "id": 188, + "name": "Channel 188", + "country": "JP", + "language": "de", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel188.png", + "stream": "https://stream.placeholder.example.com/live/channel188/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel188.xml", + "type": "catchup" + }, + { + "id": 189, + "name": "Channel 189", + "country": "BR", + "language": "fr", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel189.png", + "stream": "https://stream.placeholder.example.com/live/channel189/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel189.xml", + "type": "live" + }, + { + "id": 190, + "name": "Channel 190", + "country": "MX", + "language": "es", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel190.png", + "stream": "https://stream.placeholder.example.com/live/channel190/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel190.xml", + "type": "vod" + }, + { + "id": 191, + "name": "Channel 191", + "country": "IN", + "language": "it", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel191.png", + "stream": "https://stream.placeholder.example.com/live/channel191/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel191.xml", + "type": "catchup" + }, + { + "id": 192, + "name": "Channel 192", + "country": "NL", + "language": "ja", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel192.png", + "stream": "https://stream.placeholder.example.com/live/channel192/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel192.xml", + "type": "live" + }, + { + "id": 193, + "name": "Channel 193", + "country": "SE", + "language": "pt", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel193.png", + "stream": "https://stream.placeholder.example.com/live/channel193/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel193.xml", + "type": "vod" + }, + { + "id": 194, + "name": "Channel 194", + "country": "NO", + "language": "hi", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel194.png", + "stream": "https://stream.placeholder.example.com/live/channel194/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel194.xml", + "type": "catchup" + }, + { + "id": 195, + "name": "Channel 195", + "country": "US", + "language": "nl", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel195.png", + "stream": "https://stream.placeholder.example.com/live/channel195/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel195.xml", + "type": "live" + }, + { + "id": 196, + "name": "Channel 196", + "country": "UK", + "language": "sv", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel196.png", + "stream": "https://stream.placeholder.example.com/live/channel196/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel196.xml", + "type": "vod" + }, + { + "id": 197, + "name": "Channel 197", + "country": "CA", + "language": "no", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel197.png", + "stream": "https://stream.placeholder.example.com/live/channel197/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel197.xml", + "type": "catchup" + }, + { + "id": 198, + "name": "Channel 198", + "country": "AU", + "language": "en", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel198.png", + "stream": "https://stream.placeholder.example.com/live/channel198/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel198.xml", + "type": "live" + }, + { + "id": 199, + "name": "Channel 199", + "country": "DE", + "language": "de", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel199.png", + "stream": "https://stream.placeholder.example.com/live/channel199/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel199.xml", + "type": "vod" + }, + { + "id": 200, + "name": "Channel 200", + "country": "FR", + "language": "fr", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel200.png", + "stream": "https://stream.placeholder.example.com/live/channel200/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel200.xml", + "type": "catchup" + }, + { + "id": 201, + "name": "Channel 201", + "country": "ES", + "language": "es", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel201.png", + "stream": "https://stream.placeholder.example.com/live/channel201/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel201.xml", + "type": "live" + }, + { + "id": 202, + "name": "Channel 202", + "country": "IT", + "language": "it", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel202.png", + "stream": "https://stream.placeholder.example.com/live/channel202/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel202.xml", + "type": "vod" + }, + { + "id": 203, + "name": "Channel 203", + "country": "JP", + "language": "ja", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel203.png", + "stream": "https://stream.placeholder.example.com/live/channel203/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel203.xml", + "type": "catchup" + }, + { + "id": 204, + "name": "Channel 204", + "country": "BR", + "language": "pt", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel204.png", + "stream": "https://stream.placeholder.example.com/live/channel204/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel204.xml", + "type": "live" + }, + { + "id": 205, + "name": "Channel 205", + "country": "MX", + "language": "hi", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel205.png", + "stream": "https://stream.placeholder.example.com/live/channel205/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel205.xml", + "type": "vod" + }, + { + "id": 206, + "name": "Channel 206", + "country": "IN", + "language": "nl", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel206.png", + "stream": "https://stream.placeholder.example.com/live/channel206/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel206.xml", + "type": "catchup" + }, + { + "id": 207, + "name": "Channel 207", + "country": "NL", + "language": "sv", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel207.png", + "stream": "https://stream.placeholder.example.com/live/channel207/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel207.xml", + "type": "live" + }, + { + "id": 208, + "name": "Channel 208", + "country": "SE", + "language": "no", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel208.png", + "stream": "https://stream.placeholder.example.com/live/channel208/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel208.xml", + "type": "vod" + }, + { + "id": 209, + "name": "Channel 209", + "country": "NO", + "language": "en", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel209.png", + "stream": "https://stream.placeholder.example.com/live/channel209/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel209.xml", + "type": "catchup" + }, + { + "id": 210, + "name": "Channel 210", + "country": "US", + "language": "de", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel210.png", + "stream": "https://stream.placeholder.example.com/live/channel210/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel210.xml", + "type": "live" + }, + { + "id": 211, + "name": "Channel 211", + "country": "UK", + "language": "fr", + "category": "Sports", + "logo": "https://placeholder.example.com/logos/channel211.png", + "stream": "https://stream.placeholder.example.com/live/channel211/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel211.xml", + "type": "vod" + }, + { + "id": 212, + "name": "Channel 212", + "country": "CA", + "language": "es", + "category": "Entertainment", + "logo": "https://placeholder.example.com/logos/channel212.png", + "stream": "https://stream.placeholder.example.com/live/channel212/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel212.xml", + "type": "catchup" + }, + { + "id": 213, + "name": "Channel 213", + "country": "AU", + "language": "it", + "category": "Movies", + "logo": "https://placeholder.example.com/logos/channel213.png", + "stream": "https://stream.placeholder.example.com/live/channel213/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel213.xml", + "type": "live" + }, + { + "id": 214, + "name": "Channel 214", + "country": "DE", + "language": "ja", + "category": "Documentary", + "logo": "https://placeholder.example.com/logos/channel214.png", + "stream": "https://stream.placeholder.example.com/live/channel214/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel214.xml", + "type": "vod" + }, + { + "id": 215, + "name": "Channel 215", + "country": "FR", + "language": "pt", + "category": "Kids", + "logo": "https://placeholder.example.com/logos/channel215.png", + "stream": "https://stream.placeholder.example.com/live/channel215/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel215.xml", + "type": "catchup" + }, + { + "id": 216, + "name": "Channel 216", + "country": "ES", + "language": "hi", + "category": "Music", + "logo": "https://placeholder.example.com/logos/channel216.png", + "stream": "https://stream.placeholder.example.com/live/channel216/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel216.xml", + "type": "live" + }, + { + "id": 217, + "name": "Channel 217", + "country": "IT", + "language": "nl", + "category": "Lifestyle", + "logo": "https://placeholder.example.com/logos/channel217.png", + "stream": "https://stream.placeholder.example.com/live/channel217/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel217.xml", + "type": "vod" + }, + { + "id": 218, + "name": "Channel 218", + "country": "JP", + "language": "sv", + "category": "Tech", + "logo": "https://placeholder.example.com/logos/channel218.png", + "stream": "https://stream.placeholder.example.com/live/channel218/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel218.xml", + "type": "catchup" + }, + { + "id": 219, + "name": "Channel 219", + "country": "BR", + "language": "no", + "category": "Business", + "logo": "https://placeholder.example.com/logos/channel219.png", + "stream": "https://stream.placeholder.example.com/live/channel219/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel219.xml", + "type": "live" + }, + { + "id": 220, + "name": "Channel 220", + "country": "MX", + "language": "en", + "category": "News", + "logo": "https://placeholder.example.com/logos/channel220.png", + "stream": "https://stream.placeholder.example.com/live/channel220/playlist.m3u8", + "epg": "https://epg.placeholder.example.com/guide/channel220.xml", + "type": "vod" + } +] \ No newline at end of file diff --git a/src/utils/loadChannels.js b/src/utils/loadChannels.js new file mode 100644 index 0000000..71abbc1 --- /dev/null +++ b/src/utils/loadChannels.js @@ -0,0 +1,36 @@ +/** + * Utility to load channel data from channels.full.json + * Provides fallback mechanisms for different runtime environments + */ + +const fs = require('fs'); +const path = require('path'); + +/** + * Load channels from the JSON file + * @returns {Array} Array of channel objects, or empty array on error + */ +function loadChannels() { + try { + // Primary method: Use require for JSON (works in most Node.js environments) + try { + const channels = require('../data/channels.full.json'); + return channels; + } catch (requireError) { + // Fallback: Use fs.readFileSync for environments where require may fail + console.warn('require() failed, falling back to fs.readFileSync:', requireError.message); + + const channelsPath = path.join(__dirname, '../data/channels.full.json'); + const rawData = fs.readFileSync(channelsPath, 'utf8'); + const channels = JSON.parse(rawData); + return channels; + } + } catch (error) { + // Log error and return empty array to prevent crashes + console.error('Error loading channels:', error.message); + return []; + } +} + +// CommonJS export +module.exports = { loadChannels };