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
2 changes: 1 addition & 1 deletion .browserslistrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Chrome 132
Chrome 124
22 changes: 0 additions & 22 deletions .claude/settings.local.json

This file was deleted.

7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,10 @@ playwright-report/
.electron-vendors.cache.json

*.txt

# lens-sdk
lens-sdk
.claude

# env
.env.production
52 changes: 51 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ Flagship is Riff.CC's decentralized media platform for watching, sharing, and cu
- Environment variables control build targets
- Service layer abstracts platform differences

4. **Hybrid Data Loading Architecture (PR #70)**
- **API-First Pre-fetching**: Attempts to load data from REST API immediately for instant UI
- **Graceful P2P Fallback**: Falls back to Peerbit when API is unavailable
- **Non-Blocking P2P Init**: Peerbit initializes in background without blocking UI
- **Smart Loading Screen**: Shows appropriate loading state based on data source

Implementation details:
- Router guard in `plugins/router.ts` performs API health check
- If healthy, pre-fetches and seeds TanStack Query cache
- `composables/lensInitialization.ts` handles background P2P setup
- `getApiUrl()` dynamically constructs API URL from multiaddr
- Provides "near-instantaneous UI render" when API is available
- Degrades gracefully to P2P-only mode when necessary

4. **Component Organization**
```
packages/renderer/src/components/
Expand Down Expand Up @@ -92,4 +106,40 @@ Flagship is Riff.CC's decentralized media platform for watching, sharing, and cu
- Hot module replacement is configured for rapid development
- TypeScript is used throughout for type safety
- Vite configs in each package control build behavior
- Content is distributed via P2P network with configurable replication factors
- Content is distributed via P2P network with configurable replication factors

## CRITICAL DATA STRUCTURE NOTES

### Category and Release Structure
**Category object** has:
- `id` - The hash/unique identifier for the category
- `slug` - The slug identifier (e.g., 'tv-shows', 'music', 'movies')

**Release object** has:
- `categoryId` - References the category's hash ID
- `categorySlug` - References the category's slug

When filtering releases by category type, use `categorySlug` on the release, NOT `categoryId`!

### Structures System
Structures are completely generic organizational containers documented in `docs/STRUCTURES.md`. They can represent ANY hierarchical relationship - artists/albums, TV shows/seasons, book series/volumes, courses/lessons, etc. The system is designed for efficient PeerBit queries across arbitrary hierarchies using `parentId` relationships and content references via `metadata.structureId`.

## Key Implementation Details

### Series and Episodes
- Series structures should only exist when they have actual episodes
- Episodes link to series via `metadata.seriesId` matching the series structure's `id`
- Seasons are tracked via `metadata.seasonNumber` on episodes

### Important Reminders
- Check this file before making assumptions about data structures
- When something works on one page but not another, the issue is usually simple
- Focus on fixing exactly what's requested without adding complexity

## Important Instructions
- NEVER use git checkout to revert changes - this will throw away hours of work
- Always manually revert specific changes using the Edit tool
- Do what has been asked; nothing more, nothing less
- NEVER create files unless they're absolutely necessary
- ALWAYS prefer editing existing files to creating new ones
- Browser navigation rule: Unless explicitly stated, the USER navigates, Claude only screenshots - never use browser navigation tools without explicit permission
27 changes: 12 additions & 15 deletions Dockerfile → docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,34 @@ RUN corepack enable && corepack prepare pnpm@latest --activate
# Set working directory
WORKDIR /app

# Copy package files
# Copy package files and lens-sdk for local dependency
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY lens-sdk/ ./lens-sdk/

# Install dependencies
RUN pnpm install --frozen-lockfile --ignore-scripts
# Using --no-frozen-lockfile temporarily due to lens-sdk path mismatch
RUN pnpm install --no-frozen-lockfile --ignore-scripts --unsafe-perm

# Copy source code
# Copy remaining source code
COPY . .

# Generate Electron vendors file
RUN node scripts/update-electron-vendors.mjs

# Build the web version
RUN pnpm compile:web
RUN pnpm build

# Production stage
FROM nginx:alpine

# Copy built files from builder stage
COPY --from=builder /app/packages/renderer/dist/web /usr/share/nginx/html

# Copy nginx config if needed for SPA routing
RUN echo 'server { \
listen 80; \
server_name localhost; \
root /usr/share/nginx/html; \
index index.html; \
location / { \
try_files $uri $uri/ /index.html; \
} \
}' > /etc/nginx/conf.d/default.conf
# Copy custom nginx config for SPA routing and proper MIME types
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf

# Expose port 80
EXPOSE 80

# Start nginx
CMD ["nginx", "-g", "daemon off;"]
CMD ["nginx", "-g", "daemon off;"]
16 changes: 16 additions & 0 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;

# Static assets - serve directly or 404
location ~* \.(js|css|wasm|png|jpg|jpeg|gif|ico|svg|webp|json|txt|xml|webmanifest)$ {
try_files $uri =404;
}

# SPA fallback - only for routes, not assets
location / {
try_files $uri $uri/ /index.html;
}
}
170 changes: 170 additions & 0 deletions docs/STRUCTURES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Structures Documentation

This document details the structure system in Riff.CC - a generic hierarchical organization system for ANY type of content.

## Overview

Structures are completely generic containers that can represent ANY organizational entity and form arbitrary hierarchies. They are designed to enable efficient PeerBit queries across complex relationships.

A structure can be:
- **Artist** (containing albums, songs, collaborations)
- **Author** (containing book series, individual books)
- **Director** (containing filmographies, TV shows)
- **Actor** (containing performances across different media)
- **TV Show** (containing seasons)
- **Season** (containing episodes)
- **Album** (containing tracks)
- **Book Series** (containing volumes)
- **Course** (containing lessons)
- **Any other organizational concept**

## Data Model

### Structure Schema

```typescript
interface Structure {
id: string; // Unique identifier
type: string; // Completely arbitrary - 'artist', 'album', 'tv-show', 'season', 'course', etc.
title: string; // Display name
description?: string; // Optional description
thumbnail?: string; // CID for thumbnail image
parentId?: string; // References parent structure (completely optional)
categoryId?: string; // Optional reference to content category
categorySlug?: string; // Optional category slug
createdAt: Date;
updatedAt: Date;
}
```

### Content Relationship

Content (releases) can reference structures through metadata:

```typescript
interface Release {
// ... standard release fields
metadata: {
structureId?: string; // References ANY structure
parentStructureId?: string; // References parent structure
// ... other metadata
};
}
```

## Hierarchy Examples

### Music Organization
```
Artist: "Radiohead" (type: 'artist')
├── Album: "OK Computer" (type: 'album', parentId: radiohead-id)
│ ├── Track: "Paranoid Android" (metadata.structureId: ok-computer-id)
│ └── Track: "Karma Police" (metadata.structureId: ok-computer-id)
└── Album: "In Rainbows" (type: 'album', parentId: radiohead-id)
├── Track: "15 Step" (metadata.structureId: in-rainbows-id)
└── Track: "Bodysnatchers" (metadata.structureId: in-rainbows-id)
```

### TV Organization
```
TV Show: "Breaking Bad" (type: 'tv-show')
├── Season: "Season 1" (type: 'season', parentId: breaking-bad-id)
│ ├── Episode: "Pilot" (metadata.structureId: season-1-id)
│ └── Episode: "Cat's in the Bag..." (metadata.structureId: season-1-id)
└── Season: "Season 2" (type: 'season', parentId: breaking-bad-id)
```

### Book Series Organization
```
Author: "J.K. Rowling" (type: 'author')
└── Series: "Harry Potter" (type: 'book-series', parentId: jk-rowling-id)
├── Book: "Philosopher's Stone" (metadata.structureId: harry-potter-id)
└── Book: "Chamber of Secrets" (metadata.structureId: harry-potter-id)
```

## Query Patterns

The power of structures lies in efficient PeerBit queries across hierarchical relationships:

### Find all content under a structure
```typescript
// Find all tracks in an album
const tracks = await site.releases.query({
'metadata.structureId': albumId
});

// Find all episodes in a season
const episodes = await site.releases.query({
'metadata.structureId': seasonId
});
```

### Find all child structures
```typescript
// Find all albums by an artist
const albums = await site.structures.query({
parentId: artistId
});

// Find all seasons of a TV show
const seasons = await site.structures.query({
parentId: tvShowId
});
```

### Multi-level queries
```typescript
// Find everything by an artist (albums + standalone tracks)
const artistAlbums = await site.structures.query({ parentId: artistId });
const directTracks = await site.releases.query({ 'metadata.structureId': artistId });

// Get all tracks from all albums
const allAlbumTracks = [];
for (const album of artistAlbums) {
const tracks = await site.releases.query({ 'metadata.structureId': album.id });
allAlbumTracks.push(...tracks);
}
```

## Key Design Principles

1. **Completely Generic**: No hardcoded content types or relationships
2. **Arbitrary Hierarchies**: Any structure can be parent/child of any other
3. **Efficient P2P Queries**: Designed for optimal PeerBit query performance
4. **Optional Relationships**: All relationships are optional - structures can be standalone
5. **Flexible Metadata**: Content can reference structures however makes sense

## Common Patterns

### Standalone Content
Content doesn't need to belong to any structure:
```typescript
// A standalone documentary
{
title: "Free Culture Documentary",
// no metadata.structureId - completely independent
}
```

### Multiple Structure References
Content can reference multiple structures:
```typescript
// A song that's part of an album AND a compilation
{
title: "Bohemian Rhapsody",
metadata: {
structureId: albumId, // Part of "A Night at the Opera"
compilationIds: [comp1, comp2] // Also in various compilations
}
}
```

### Cross-Category Structures
Structures can span different content categories:
```typescript
// A director structure containing both movies and TV shows
Director: "Christopher Nolan" (type: 'director')
├── Movie: "Inception" (categorySlug: 'movies', metadata.structureId: nolan-id)
├── Movie: "Interstellar" (categorySlug: 'movies', metadata.structureId: nolan-id)
└── TV Show: "Westworld" (categorySlug: 'tv-shows', metadata.structureId: nolan-id)
```
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@rollup/plugin-node-resolve": "^16.0.1",
"@types/luxon": "^3.6.2",
"@types/node": "^22.15.33",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.35.0",
"@typescript-eslint/parser": "^8.35.0",
"@vitejs/plugin-vue": "^5.2.4",
Expand Down Expand Up @@ -71,13 +72,15 @@
},
"dependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@riffcc/lens-sdk": "^0.1.32",
"@multiformats/multiaddr": "^12.5.1",
"@riffcc/lens-sdk": "file:../lens-sdk",
Copy link

Copilot AI Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a local file path dependency instead of a published version. This should be changed back to a proper version number before production deployment to ensure reproducible builds.

Suggested change
"@riffcc/lens-sdk": "file:../lens-sdk",
"@riffcc/lens-sdk": "^1.0.0",

Copilot uses AI. Check for mistakes.
"@tanstack/vue-query": "^5.81.2",
"@vueuse/core": "^12.8.2",
"core-js": "^3.43.0",
"electron-updater": "^6.6.2",
"events": "^3.3.0",
"is-ipfs": "^8.0.4",
"jsmediatags": "^3.9.7",
"luxon": "^3.6.1",
"multiformats": "^13.3.7",
"vue": "^3.5.17",
Expand Down
3 changes: 3 additions & 0 deletions packages/main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ app
ipcMain.handle('peerbit:add-release', async (_event, releaseData: ReleaseData) =>
lensService?.addRelease(releaseData),
);
ipcMain.handle('peerbit:edit-release', async (_event, releaseData: ReleaseData) =>
Copy link

Copilot AI Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type annotation uses ReleaseData but the preload file imports EditInput<ReleaseData>. The main process should use the same type for consistency and proper type safety.

Suggested change
ipcMain.handle('peerbit:edit-release', async (_event, releaseData: ReleaseData) =>
ipcMain.handle('peerbit:edit-release', async (_event, releaseData: EditInput<ReleaseData>) =>

Copilot uses AI. Check for mistakes.
lensService?.editRelease(releaseData),
);
ipcMain.handle('peerbit:get-release', async (_event, id: string) =>
lensService?.getRelease({ id }),
);
Expand Down
3 changes: 2 additions & 1 deletion packages/preload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { contextBridge, ipcRenderer } from 'electron';
import { isLinux, isMac, isWindows, platform} from './so';
import type { ReleaseData, HashResponse, Release } from '@riffcc/lens-sdk';
import type { ReleaseData, HashResponse, Release, EditInput } from '@riffcc/lens-sdk';

contextBridge.exposeInMainWorld('osInfo', {
isMac,
Expand All @@ -25,6 +25,7 @@ contextBridge.exposeInMainWorld('electronLensService', {
getPeerId: (): Promise<string> => ipcRenderer.invoke('peerbit:get-peer-id'),
dial: (address: string): Promise<boolean> => ipcRenderer.invoke('peerbit:dial', address),
addRelease: (releaseData: ReleaseData): Promise<HashResponse> => ipcRenderer.invoke('peerbit:add-release', releaseData),
editRelease: (releaseData: EditInput<ReleaseData>): Promise<HashResponse> => ipcRenderer.invoke('peerbit:edit-release', releaseData),
getRelease: (id: string): Promise<Release | undefined> => ipcRenderer.invoke('peerbit:get-release', id),
getLatestReleases: (size?: number): Promise<Release[]> => ipcRenderer.invoke('peerbit:get-latest-releases', size),
});
Loading