Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/playwright-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ env:
UPSPLASH_URL: ''
YOUTUBE_API_KEY: ''
CONVERT_KIT_V4_API_KEY: ''
MEGAPHONE_API_TOKEN: ''
MEGAPHONE_NETWORK_ID: ''
MEGAPHONE_PODCAST_ID: ''

jobs:
test:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/stylelint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ env:
UPSPLASH_URL: ''
YOUTUBE_API_KEY: ''
CONVERT_KIT_V4_API_KEY: ''
MEGAPHONE_API_TOKEN: ''
MEGAPHONE_NETWORK_ID: ''
MEGAPHONE_PODCAST_ID: ''

jobs:
test:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/svelte_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ env:
TURNSTILE_SECRET: ''
PUBLIC_TURNSTILE_SITE_KEY: ''
CONVERT_KIT_V4_API_KEY: ''
MEGAPHONE_API_TOKEN: ''
MEGAPHONE_NETWORK_ID: ''
MEGAPHONE_PODCAST_ID: ''

jobs:
test:
Expand Down
194 changes: 194 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Development Commands

### Core Commands
- `pnpm dev` - Start development server with preheat script
- `pnpm build` - Build production application (runs svelte build + file copying)
- `pnpm check` - Run TypeScript type checking
- `pnpm lint` - Run prettier + eslint + stylelint
- `pnpm test` - Run Playwright tests
- `pnpm test:unit` - Run Vitest unit tests

### Database Commands
- `pnpm db:studio` - Open Prisma Studio GUI
- `pnpm db:generate` - Generate Prisma client
- `pnpm db:push` - Push schema changes to database
- `pnpm db:seed` - Seed database with test data
- `pnpm i-changed-the-schema` - Shortcut for push + generate after schema changes

### Testing Commands
- `pnpm test:ui` - Run Playwright tests with UI
- `pnpm check:watch` - Run TypeScript check in watch mode

## Project Architecture

### Technology Stack
- **Frontend**: SvelteKit with Svelte 5, TypeScript
- **Backend**: Node.js with SvelteKit server-side rendering
- **Database**: MySQL with Prisma ORM (PlanetScale)
- **Caching**: Redis (Upstash)
- **Deployment**: Vercel with Node.js 22.x runtime
- **Monitoring**: Sentry for error tracking

### Key Directory Structure
```
src/
├── routes/
│ ├── (site)/ # Main website layout
│ ├── (blank)/ # Clean layout for embeds
│ └── api/ # API endpoints
├── lib/ # Reusable components
├── server/ # Server-side logic and utilities
├── state/ # Svelte stores for client state
├── styles/ # CSS architecture with themes
├── actions/ # Svelte actions
└── utilities/ # Shared utilities
```

### Database Schema
The application manages podcast content with these key models:
- **Show**: Episodes with metadata, transcripts, AI-generated content
- **User**: GitHub OAuth authentication with role-based access
- **Guest**: Guest profiles with social links and appearances
- **Transcript**: Full transcription with utterances and speaker identification
- **Video**: YouTube integration with playlists
- **UserSubmission**: User-generated content submissions

### Content Management
- Show notes are stored as markdown files in the `/shows/` directory
- AI-generated content (summaries, tweets, show notes) using OpenAI/Anthropic
- Automated transcription via Deepgram
- Multi-layer caching with Redis for performance

## Code Style and Conventions

### Naming Conventions
- **Components**: PascalCase for `.svelte` files (e.g., `ShowCard.svelte`)
- **Variables/Functions**: snake_case for variables, functions, and props
- **Constants**: UPPER_CASE for true constants only
- **Types**: PascalCase for TypeScript interfaces

### Svelte 5 Patterns
- Use `$state` for reactive state declarations
- Use `$derived` for computed values
- Use `$effect` for side effects and lifecycle
- Use `$props` for component props with destructuring
- Use `$bindable` for two-way bindable props
- Use classes for complex state management (state machines)

### CSS Architecture
- CSS variables defined in `src/styles/variables.css`
- Use `bg` and `fg` convention for background/foreground colors
- Custom media queries for responsive design:
```css
@custom-media --below-med (width < 700px);
@custom-media --above-med (width > 700px);
```

### File Organization
- **Components**: Group related components in `/src/lib/`
- **Routes**: Use SvelteKit's file-based routing with layout groups
- **State**: Svelte stores in `/src/state/` for global state
- **Server**: All server-side logic in `/src/server/`

## Key Features

### Audio Player
- Advanced web audio player with offline support
- Media Session API integration
- Position saving and resume functionality
- Service worker for offline playback

### Search System
- Client-side search using FlexSearch
- Web workers for non-blocking search
- Fuzzy search across shows, guests, and transcripts

### Admin Dashboard
- Role-based access control
- Content management for shows and guests
- AI content generation tools
- Transcript management

### Performance Optimization
- Redis caching for database queries
- IndexedDB for offline data storage
- Aggressive caching strategies
- Service worker for offline functionality

## Development Workflow

### Adding New Features
1. Create components in `/src/lib/` for reusable UI
2. Add routes in `/src/routes/` following the layout structure
3. Use Prisma for database operations via `/src/server/prisma-client.ts`
4. Implement caching for database queries using the cache utilities
5. Add proper TypeScript types and error handling

### Database Changes
1. Modify `/prisma/schema.prisma`
2. Run `pnpm i-changed-the-schema` to apply changes
3. Update seed data if needed in `/prisma/seed.ts`

### Testing
- Use Playwright for integration tests
- Use Vitest for unit tests
- Run linting before commits
- Test across different screen sizes and devices

### Deployment
- Application deploys to Vercel automatically
- Environment variables managed through Vercel dashboard
- Database hosted on PlanetScale
- Redis cache on Upstash

## Common Patterns

### State Management
```typescript
// For complex state, use classes
class PlayerState {
currentShow = $state<Show | null>(null);
isPlaying = $state(false);

play() {
this.isPlaying = true;
}
}

export const playerState = new PlayerState();
```

### Database Queries
```typescript
// Use the cached prisma client
import { prisma } from '$server/prisma-client';

// Cache database queries
const shows = await prisma.show.findMany({
where: { show_type: 'TASTY' },
orderBy: { number: 'desc' }
});
```

### Component Structure
```svelte
<script lang="ts">
import { playerState } from '$state/player.svelte';

let { show } = $props<{ show: Show }>();
let isPlaying = $derived(playerState.currentShow?.id === show.id);
</script>

<div class="show-card">
<h3>{show.title}</h3>
<button onclick={() => playerState.play(show)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
```

Remember to follow the existing code conventions and patterns when adding new features or modifying existing code.
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ model Show {
date DateTime
url String
youtube_url String?
spotify_id String?
show_notes String @db.Text
hash String @unique
slug String
Expand Down
138 changes: 138 additions & 0 deletions scripts/sync-spotify-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env node

/**
* Sync Spotify Data Script
*
* This script syncs Spotify data for all episodes using the built syncEpisodeSpotifyData function
* and tracks episodes that could not be matched.
*/

import { PrismaClient } from '@prisma/client';
import { config } from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

// Load environment variables
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
config({ path: join(__dirname, '..', '.env') });

const prisma = new PrismaClient();

async function syncSpotifyData() {
console.log('🎵 Starting Spotify data sync for all episodes...\n');

try {
// Import the sync function and types
const { syncEpisodeSpotifyData } = await import('../src/server/megaphone/sync.ts');

// Create credentials object
const credentials = {
apiToken: process.env.MEGAPHONE_API_TOKEN,
networkId: process.env.MEGAPHONE_NETWORK_ID,
podcastId: process.env.MEGAPHONE_PODCAST_ID
};

// Get all episodes from database that don't have Spotify IDs
const shows = await prisma.show.findMany({
where: { spotify_id: null },
select: { number: true, title: true, spotify_id: true },
orderBy: { number: 'desc' }
});

console.log(`Found ${shows.length} episodes without Spotify IDs\n`);

if (shows.length === 0) {
console.log('✅ All episodes already have Spotify IDs!');
return;
}

// Track results
const results = {
processed: 0,
updated: 0,
unmatched: [],
errors: []
};

console.log('Processing episodes...\n');

// Process each show
for (const show of shows) {
try {
console.log(`Processing episode ${show.number}: ${show.title}`);
results.processed++;

// Use the existing sync function
await syncEpisodeSpotifyData(show.number, credentials);

// Check if it was updated
const updatedShow = await prisma.show.findUnique({
where: { number: show.number },
select: { spotify_id: true }
});

if (updatedShow?.spotify_id) {
results.updated++;
console.log(` ✅ Updated with Spotify ID: ${updatedShow.spotify_id}`);
} else {
results.unmatched.push({
number: show.number,
title: show.title
});
console.log(` ❌ No match found or no Spotify ID available`);
}
} catch (error) {
results.errors.push({
number: show.number,
title: show.title,
error: error.message
});
console.error(` ❌ Error processing episode ${show.number}: ${error.message}`);
}

// Add a small delay to avoid overwhelming the API
await new Promise((resolve) => setTimeout(resolve, 100));
}

// Final results
console.log('\n' + '='.repeat(80));
console.log('📊 SYNC RESULTS:');
console.log('='.repeat(80));
console.log(`Total episodes processed: ${results.processed}`);
console.log(`✅ Successfully updated: ${results.updated}`);
console.log(`❌ Unmatched episodes: ${results.unmatched.length}`);
console.log(`🚨 Errors: ${results.errors.length}`);
console.log(`📈 Success rate: ${((results.updated / results.processed) * 100).toFixed(1)}%`);

if (results.unmatched.length > 0) {
console.log('\n❌ UNMATCHED EPISODES:');
results.unmatched.forEach((episode) => {
console.log(` ${episode.number}: ${episode.title}`);
});
}

if (results.errors.length > 0) {
console.log('\n🚨 ERRORS:');
results.errors.forEach((error) => {
console.log(` ${error.number}: ${error.title} - ${error.error}`);
});
}

if (results.unmatched.length > 0 || results.errors.length > 0) {
console.log('\n🔧 RECOMMENDATIONS:');
console.log('- Check if unmatched episodes exist on Spotify');
console.log('- Review episode titles for formatting differences');
console.log('- Verify episode numbering consistency');
console.log('- Check Megaphone API response for completeness');
}
} catch (error) {
console.error('❌ Fatal error:', error.message);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}

// Run the script
syncSpotifyData();
4 changes: 3 additions & 1 deletion src/lib/ListenLinks.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
target="_blank"
title="Watch or Listen on Spotify"
aria-label="Spotify"
href="https://open.spotify.com/search/syntax.fm {encodeURI(show.title)}/episodes"
href={show.spotify_id
? `https://open.spotify.com/episode/${show.spotify_id}`
: `https://open.spotify.com/search/syntax.fm ${encodeURI(show.title)}/episodes`}
>
<Icon name="spotify" />
</a>
Expand Down
Loading