Skip to content
Merged
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
41 changes: 36 additions & 5 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,18 @@ Response: { "user": { ... }, "group": { ... } }
| PATCH | `/api/clips/[id]` | Update clip title |
| DELETE | `/api/clips/[id]` | Remove clip |
| GET | `/api/clips/unwatched-count` | Count of unwatched clips |
| POST | `/api/clips/dismiss` | Dismiss unwatched clips (bulk) |
| DELETE | `/api/clips/dismiss` | Restore dismissed clips |
| GET | `/api/clips/dismissed` | List dismissed clips for user |

### GET /api/clips
```
Query params: ?filter=unwatched|watched|favorites|uploads&sort=oldest|round-robin&limit=20&offset=0
Query params: ?filter=unwatched|watched|favorites|uploads&sort=oldest|round-robin|best&limit=20&offset=0
Response: { "clips": [...], "hasMore": true }
```
Only returns clips with `status: 'ready'`. Default sort is `oldest` (chronological). `round-robin` interleaves clips across members so no single poster dominates the feed. The `watched` filter sorts by most-recently-watched instead.
Only returns clips with `status: 'ready'`. Default sort is `oldest` (chronological). `round-robin` interleaves clips across members so no single poster dominates the feed. `best` ranks clips by engagement (reactions + comments + views) with source view count as tiebreaker and round-robin balance. The `watched` filter sorts by most-recently-watched instead.

Each clip includes: id, originalUrl, title, addedByUsername, addedByAvatar, status, durationSeconds, platform, contentType, creatorName, creatorUrl, createdAt, watched, favorited, reactions, commentCount, unreadCommentCount, viewCount, seenByOthers.
Each clip includes: id, originalUrl, title, addedByUsername, addedByAvatar, status, durationSeconds, platform, contentType, creatorName, creatorUrl, sourceViewCount, createdAt, watched, favorited, reactions, commentCount, unreadCommentCount, viewCount, seenByOthers.

### POST /api/clips
```
Expand All @@ -93,7 +96,7 @@ Also accepts `"phones": ["+1234567890"]` (array) for legacy Shortcut backward co
### GET /api/clips/[id]
Returns full clip detail with user context, interaction state, and metadata.
```
Response: { id, originalUrl, videoPath, audioPath, thumbnailPath, title, artist, albumArt, spotifyUrl, appleMusicUrl, youtubeMusicUrl, addedBy, addedByUsername, addedByAvatar, platform, status, contentType, durationSeconds, creatorName, creatorUrl, watched, favorited, reactions, commentCount, unreadCommentCount, viewCount, seenByOthers, createdAt, canEditCaption }
Response: { id, originalUrl, videoPath, audioPath, thumbnailPath, title, artist, albumArt, spotifyUrl, appleMusicUrl, youtubeMusicUrl, addedBy, addedByUsername, addedByAvatar, platform, status, contentType, durationSeconds, creatorName, creatorUrl, sourceViewCount, watched, favorited, reactions, commentCount, unreadCommentCount, viewCount, seenByOthers, createdAt, canEditCaption }
```

### PATCH /api/clips/[id]
Expand All @@ -111,6 +114,27 @@ Host can delete any clip. Non-host uploaders can only delete their own clips if
Response: { "count": 5 }
```

### POST /api/clips/dismiss
Bulk-dismiss unwatched clips. Skips already-watched and already-dismissed clips.
```
Request: { "keepIds": ["clip-1", "clip-2"] } (optional clips to exclude from dismissal)
Response: { "dismissed": 3 }
```

### DELETE /api/clips/dismiss
Restore previously dismissed clips.
```
Request: { "clipIds": ["id1", "id2"] } (specific clips)
Request: { "all": true } (restore all dismissed clips)
Response: { "restored": 2 }
```

### GET /api/clips/dismissed
```
Response: { "clips": [...], "count": 3 }
```
Returns dismissed clips with thumbnail, platform, uploader info, and dismissal timestamp.

## Interactions

| Method | Path | Description |
Expand Down Expand Up @@ -185,7 +209,7 @@ Response: { "status": "downloading" }
### POST /api/clips/[id]/refetch
Host-only. Refetches metadata (title, creator info) from the source URL via yt-dlp.
```
Response: { "title": "...", "creatorName": "...", "creatorUrl": "..." }
Response: { "title": "...", "creatorName": "...", "creatorUrl": "...", "sourceViewCount": 12345 }
```

### POST /api/clips/[id]/trim
Expand Down Expand Up @@ -224,6 +248,7 @@ Host-only endpoints (unless noted). Requires `createdBy === currentUser`.
| PATCH | `/api/group/retention` | Set retention policy |
| PATCH | `/api/group/max-file-size` | Set max file size limit |
| PATCH | `/api/group/platforms` | Set platform filter |
| PATCH | `/api/group/daily-share-limit` | Set daily share limit per user |
| GET | `/api/group/provider` | List download providers |
| PATCH | `/api/group/provider` | Set active provider |
| POST | `/api/group/provider/install` | Install a provider |
Expand Down Expand Up @@ -267,6 +292,12 @@ Request: { "mode": "all", "platforms": [] } (mode: "all" | "allow" | "block")
Response: { "platformFilterMode": "all", "platformFilterList": null }
```

### PATCH /api/group/daily-share-limit
```
Request: { "dailyShareLimit": 5 } (positive integer, or null to remove limit)
Response: { "dailyShareLimit": 5 }
```

### GET /api/group/provider
```
Response: { "providers": [{ "id", "name", "installed", "version", ... }] }
Expand Down
8 changes: 8 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ scrolly/
│ │ │ │ └── verify.ts # Twilio SMS verification codes
│ │ │ ├── auth.ts # Session management, invite code validation
│ │ │ ├── push.ts # web-push wrapper, group notifications
│ │ │ ├── share-limit.ts # Daily share limit enforcement utility
│ │ │ ├── scheduler.ts # Retention policy enforcement (periodic cleanup)
│ │ │ └── download-lock.ts # Prevents duplicate concurrent downloads
│ │ ├── components/
Expand All @@ -95,6 +96,7 @@ scrolly/
│ │ │ ├── AddVideo.svelte # Add video form
│ │ │ ├── AddVideoModal.svelte # Modal wrapper for AddVideo
│ │ │ ├── AvatarCropModal.svelte # Profile picture crop UI
│ │ │ ├── CatchUpModal.svelte # Catch-up modal for bulk unwatched clips
│ │ │ ├── MeGrid.svelte # Profile clip grid (favorites/uploads)
│ │ │ ├── MeReelView.svelte # Profile reel overlay view
│ │ │ ├── CommentInput.svelte # Rich comment input with GIF and mention support
Expand All @@ -115,13 +117,16 @@ scrolly/
│ │ │ ├── PlatformIcon.svelte # Platform logo (TikTok, IG, etc.)
│ │ │ ├── InlineError.svelte
│ │ │ ├── FilterBar.svelte # Feed filter tabs
│ │ │ ├── ShareLimitDots.svelte # Daily share limit indicator dots
│ │ │ ├── ShortcutGuideSheet.svelte # iOS Shortcut setup guide
│ │ │ ├── ShortcutUpgradeBanner.svelte # Legacy shortcut upgrade prompt
│ │ │ └── settings/
│ │ │ ├── GroupNameEdit.svelte
│ │ │ ├── InviteLink.svelte
│ │ │ ├── MemberList.svelte
│ │ │ ├── DailyShareLimitPicker.svelte # Daily per-user share limit control
│ │ │ ├── RetentionPicker.svelte
│ │ │ ├── SkippedClips.svelte # Dismissed/skipped clips viewer with restore
│ │ │ ├── ClipsManager.svelte
│ │ │ ├── NotificationSettings.svelte # Push toggle + test button
│ │ │ ├── ShortcutManager.svelte # iOS Shortcut config wrapper
Expand All @@ -142,6 +147,7 @@ scrolly/
│ │ │ ├── sheetOpen.ts # Global any-sheet-open state (blocks scroll/nav)
│ │ │ ├── uiHidden.ts # Feed UI hidden state (synced from active reel)
│ │ │ ├── homeTap.ts # Double-tap home to scroll to top
│ │ │ ├── catchUpModal.ts # Catch-up modal dismissal state (12-hour cooldown)
│ │ │ ├── shortcutNudge.ts # Share shortcut install nudge
│ │ │ └── shortcutUpgrade.ts # Shortcut upgrade banner state
│ │ ├── types.ts # Shared TypeScript types (Clip, etc.)
Expand All @@ -163,6 +169,8 @@ scrolly/
│ │ ├── api/ # REST API (see docs/api.md)
│ │ │ ├── auth/
│ │ │ ├── clips/
│ │ │ │ ├── dismiss/+server.ts # Dismiss/restore unwatched clips
│ │ │ │ ├── dismissed/+server.ts # List dismissed clips
│ │ │ │ └── [id]/refetch/+server.ts # Host-only metadata refetch
│ │ │ │ └── [id]/trim/+server.ts # Music clip trim
│ │ │ │ └── [id]/waveform/+server.ts # Waveform data
Expand Down
14 changes: 14 additions & 0 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ SQLite database via Drizzle ORM. All IDs are UUIDs stored as text. Timestamps ar
| download_provider | text | Nullable. Active download provider ID. |
| platform_filter_mode | text | Default `'all'`. `'all'` / `'allow'` / `'block'`. |
| platform_filter_list | text | Nullable. Comma-separated list of platforms for allow/block filtering. |
| daily_share_limit | integer | Nullable. Max clips per user per calendar day. |
| shortcut_token | text | Nullable, unique. Token for iOS Shortcut clip sharing. |
| shortcut_url | text | Nullable. URL for iOS Shortcut integration. |
| created_by | text | FK → users.id (host/admin) |
Expand Down Expand Up @@ -66,6 +67,7 @@ SQLite database via Drizzle ORM. All IDs are UUIDs stored as text. Timestamps ar
| file_size_bytes | integer | Nullable. File size for storage tracking. |
| creator_name | text | Nullable. Original content creator name (from yt-dlp metadata). |
| creator_url | text | Nullable. Original content creator profile URL. |
| source_view_count | integer | Nullable. View count from original source (yt-dlp metadata). |
| created_at | integer | Unix timestamp |

Unique index on `(group_id, original_url)` — prevents duplicate URLs within a group.
Expand Down Expand Up @@ -148,6 +150,16 @@ Index on `(user_id, created_at)` for efficient feed queries.

Unique constraint on `(clip_id, user_id)` — tracks whether a user has seen the comments on a clip.

### dismissed_clips

| Column | Type | Notes |
|--------|------|-------|
| clip_id | text | Composite PK, FK → clips.id |
| user_id | text | Composite PK, FK → users.id |
| dismissed_at | integer | Unix timestamp when dismissed |

Unique constraint on `(clip_id, user_id)` — tracks clips dismissed by users in catch-up modal.

### push_subscriptions

| Column | Type | Notes |
Expand Down Expand Up @@ -198,6 +210,7 @@ comments 1──∞ comment_hearts
clips ∞──∞ users (watched)
clips ∞──∞ users (favorites)
clips ∞──∞ users (comment_views)
clips ∞──∞ users (dismissed_clips)
users 1──∞ push_subscriptions
users 1──1 notification_preferences
users 1──∞ notifications (recipient)
Expand All @@ -215,3 +228,4 @@ users 1──∞ verification_codes
- **Music clips:** The `content_type` field distinguishes video clips from music links. Music clips have additional fields for cross-platform streaming URLs resolved via Odesli.
- **Duplicate URL prevention:** A unique index on `(group_id, original_url)` prevents the same link from being shared twice within a group.
- **Music clip trim workflow:** Music clips enter `pending_trim` status after download. The user can trim audio via the trim UI or skip trimming. If neither occurs before `trim_deadline`, the clip auto-publishes to `ready` status via the scheduler.
- **Dismissed clips:** The `dismissed_clips` table tracks clips dismissed by users in the catch-up modal. Users can dismiss unwatched clips in bulk, then restore them later from the Skipped Clips viewer in settings.