Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
057ce55
feat(sermon-audio): enable Cross Publish on publish with connection-a…
threehappypenguins Jun 30, 2026
6a57a42
Merge branch 'dev' into feat/sa-cross-publishing
threehappypenguins Jun 30, 2026
92376bc
docs(sermon-audio): clarify useVideoClip applies to Facebook and X
threehappypenguins Jun 30, 2026
2da3038
Merge pull request #80 from threehappypenguins/feat/sa-cross-publishing
threehappypenguins Jun 30, 2026
cd09d44
feat(drafts): add organizational labels with colors and mobile-friend…
threehappypenguins Jun 30, 2026
4ea9238
fix(drafts): harden label API validation, add route tests
threehappypenguins Jul 1, 2026
eb20907
fix(drafts): avoid partial-success failures when syncing draft labels
threehappypenguins Jul 1, 2026
e75f544
fix(drafts): bulk label cascade removal, map labels API 404s, and fix…
threehappypenguins Jul 1, 2026
4694f96
fix(drafts): tighten label validation, throw 404 for missing profile,…
threehappypenguins Jul 1, 2026
3d9ea5d
Merge pull request #81 from threehappypenguins/bug/mobile-overlapping…
threehappypenguins Jul 1, 2026
3430fd1
feat(sermon-audio): add publishTimestamp scheduling with scheduled up…
threehappypenguins Jul 1, 2026
89bfae2
fix(sermon-audio): restore auto-publish default when clearing schedule
threehappypenguins Jul 1, 2026
2f3bfb9
Merge pull request #83 from threehappypenguins/feat/sa-publish-schedule
threehappypenguins Jul 1, 2026
a50efaf
feat(sermon-audio): add Instagram as a Cross Publish destination
threehappypenguins Jul 1, 2026
6ddcb15
Merge pull request #84 from threehappypenguins/feat/sa-cross-publish-…
threehappypenguins Jul 1, 2026
3d75d10
fix(scheduling): add single-field free-form time entry and fix schedu…
threehappypenguins Jul 1, 2026
3e54893
Merge pull request #85 from threehappypenguins/bug/fix-time-adder
threehappypenguins Jul 1, 2026
51e4f11
refactor(dashboard): rename drafts to videos, nest history routes, an…
threehappypenguins Jul 1, 2026
35443f2
fix(dashboard): derive nav drawer state, reconcile streamed history o…
threehappypenguins Jul 2, 2026
0e2a677
test(livestreams): add streamed GET pagination tests and document She…
threehappypenguins Jul 2, 2026
db48457
Merge pull request #86 from threehappypenguins/refactor/reorganize-links
threehappypenguins Jul 2, 2026
f927028
feat(youtube-import): add end-to-end YouTube import, trim, and draft …
threehappypenguins Jul 2, 2026
4411641
feat(youtube-import): defer platform upload until Upload & Save with …
threehappypenguins Jul 2, 2026
73535a3
fix(youtube-import): list streamed broadcasts, row thumbnails, and ha…
threehappypenguins Jul 2, 2026
4ab0a81
feat(youtube-import): proxied HTML5 trim preview, confirmed cancel, a…
threehappypenguins Jul 2, 2026
1b56618
fix(youtube-import): paginate import picker two at a time and harden …
threehappypenguins Jul 2, 2026
2921a31
fix(youtube-import): schedule imports server-side and remove worker s…
threehappypenguins Jul 2, 2026
66031c0
fix(livestreams): paginate history/import lists in MongoDB and bound …
threehappypenguins Jul 2, 2026
d8c298d
fix(youtube-import): interrupt downloads on cancel and harden start, …
threehappypenguins Jul 2, 2026
2e1030f
fix(youtube-import): refresh near-expiry previews and backoff cancel …
threehappypenguins Jul 2, 2026
ef4981a
Merge pull request #88 from threehappypenguins/feat/youtube-download
threehappypenguins Jul 2, 2026
f085a13
fix(youtube-import): show friendly private-video errors and isolate t…
threehappypenguins Jul 2, 2026
49ddeca
docs: add local docker image testing guide
threehappypenguins Jul 2, 2026
b0792cb
chore: upgraded minor version number
threehappypenguins Jul 2, 2026
49e9edb
refactor(dashboard): rename Videos to Uploads and move routes to /das…
threehappypenguins Jul 2, 2026
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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ R2_BUCKET_NAME=videosphere-uploads
# Optional: restrict allowed browser origin for R2 CORS behavior in your app logic
# R2_ALLOWED_ORIGIN=https://your-domain.com

# -----------------------------------------------------------------------------
# YouTube import (download + trim from connected YouTube account)
# -----------------------------------------------------------------------------
# Scratch directory for in-progress downloads and trims (default: /tmp/yt-import)
# YT_IMPORT_WORKDIR=/tmp/yt-import
# Hard cap on source video length in seconds (default: 14400 / 4h)
# YT_IMPORT_MAX_DURATION_SECONDS=14400

# -----------------------------------------------------------------------------
# OpenRouter (AI metadata generation)
# -----------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile .

Omit `--load` and `--push` to discard images after a successful build. Cross-arch builds on amd64 require QEMU (`qemu-user-static` and binfmt).

To **build and run** the image locally (amd64 smoke test, OAuth, production server), see [docs/local-docker-testing.md](docs/local-docker-testing.md).

## Pull Request Process

1. **Fill out the PR template** — it's provided automatically when you open a PR
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build

FROM node:${NODE_VERSION}-alpine AS runner
RUN apk add --no-cache ffmpeg
ARG YT_DLP_VERSION=2026.2.21
RUN apk add --no-cache ffmpeg python3 py3-pip \
&& pip3 install --break-system-packages "yt-dlp[default]==${YT_DLP_VERSION}" \
&& yt-dlp --version && ffmpeg -version
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
Expand Down
14 changes: 11 additions & 3 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,18 @@ an explicit Compose env file, so this command must include `--env-file .env.loca

### Option B: Docker run (also valid for local development)

If you prefer running Mongo directly, this is supported:
If you prefer running Mongo directly, this is supported. Use the **fully qualified** image name — Podman does not resolve short names like `mongo:8` unless your registry config defines search registries (Docker Desktop does this automatically).

```bash
docker pull docker.io/mongo:8
docker pull docker.io/library/mongo:8

docker run -d \
--name videosphere-mongo \
-p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=localdevpassword \
-v videosphere-mongo-data:/data/db \
mongo:8
docker.io/library/mongo:8
```

When using this option, ensure your `.env.local` uses matching credentials, for example:
Expand Down Expand Up @@ -183,6 +183,14 @@ With Docker Compose, uncomment `network_mode: host` on the `app` service (see `d

`pnpm dev` on the host (without Docker) can reach the LAN directly; host networking is only required when the app runs inside a container.

## Testing a local Docker build

To build the production image and run it on your machine (instead of `pnpm dev`), see [docs/local-docker-testing.md](docs/local-docker-testing.md). Typical amd64 build:

```bash
./scripts/docker-build-platform.sh linux/amd64 videosphere:amd64-test
```

## Notes

- This repository uses MongoDB for auth/session-related data and application persistence.
Expand Down
22 changes: 22 additions & 0 deletions __tests__/api/drafts/drafts-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@ vi.mock('@/lib/r2', async (importOriginal) => {
};
});

vi.mock('@/lib/repositories/users', () => ({
upsertDraftLabelsInLibrary: vi.fn(),
}));

import { GET, PATCH, DELETE } from '@/app/api/drafts/[id]/route';
import { getAuthenticatedUserId } from '@/lib/api/auth';
import { getDraftById, updateDraft, deleteDraft } from '@/lib/repositories/drafts';
import { upsertDraftLabelsInLibrary } from '@/lib/repositories/users';
import { deleteObject, buildDraftThumbnailFinalKey } from '@/lib/r2';
import { DraftDocumentTooLargeError, MAX_DRAFT_TITLE_LENGTH } from '@/lib/draft-upload-metadata';

Expand Down Expand Up @@ -472,6 +477,23 @@ describe('PATCH /api/drafts/[id]', () => {
expect(updateDraft).toHaveBeenCalledWith(DRAFT_ID, { tags: ['a', 'b'] });
});

it('returns 200 when label library upsert fails after the draft is updated', async () => {
const updated = { ...baseDraft, labels: ['Sunday'] };
vi.mocked(updateDraft).mockResolvedValueOnce(updated);
vi.mocked(upsertDraftLabelsInLibrary).mockRejectedValueOnce(new Error('db down'));

const res = await PATCH(
makeRequest('PATCH', { labels: ['Sunday'] }, { [SESSION_COOKIE]: 'tok' }),
makeParams()
);

expect(res.status).toBe(200);
const body = await res.json();
expect(body.message).toBe('Draft updated');
expect(body.data.labels).toEqual(['Sunday']);
expect(upsertDraftLabelsInLibrary).toHaveBeenCalledWith('user-123', ['Sunday']);
});

it('updates multiple fields at once', async () => {
const updated = {
...baseDraft,
Expand Down
24 changes: 24 additions & 0 deletions __tests__/api/drafts/drafts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@ vi.mock('@/lib/repositories/upload-jobs', () => ({
listUploadJobsByUserForDraftIds: vi.fn(async () => []),
}));

vi.mock('@/lib/repositories/users', () => ({
upsertDraftLabelsInLibrary: vi.fn(),
}));

import { POST, GET } from '@/app/api/drafts/route';
import { getAuthenticatedUserId } from '@/lib/api/auth';
import { createDraft, listDraftsByUser, markDraftUsedInUpload } from '@/lib/repositories/drafts';
import { listUploadJobsByUserForDraftIds } from '@/lib/repositories/upload-jobs';
import { upsertDraftLabelsInLibrary } from '@/lib/repositories/users';
import { DraftDocumentTooLargeError, MAX_DRAFT_TITLE_LENGTH } from '@/lib/draft-upload-metadata';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -305,6 +310,7 @@ describe('POST /api/drafts', () => {
title: 'My Video',
description: 'Great video',
tags: [],
labels: [],
targets: ['youtube', 'vimeo'],
platforms: {},
backupNaming: normalizeBackupFileNameSettings(undefined),
Expand Down Expand Up @@ -353,6 +359,24 @@ describe('POST /api/drafts', () => {

expect(createDraft).toHaveBeenCalledWith(expect.objectContaining({ userId: 'user-123' }));
});

it('returns 201 when label library upsert fails after the draft is created', async () => {
vi.mocked(createDraft).mockResolvedValueOnce({ ...baseDraft, labels: ['Sunday'] });
vi.mocked(upsertDraftLabelsInLibrary).mockRejectedValueOnce(new Error('db down'));

const req = makeRequest(
'POST',
{ title: 'My Video', targets: ['youtube'], labels: ['Sunday'] },
{ [SESSION_COOKIE]: 'tok' }
);
const res = await POST(req);

expect(res.status).toBe(201);
const body = await res.json();
expect(body.message).toBe('Draft created');
expect(body.data.labels).toEqual(['Sunday']);
expect(upsertDraftLabelsInLibrary).toHaveBeenCalledWith('user-123', ['Sunday']);
});
});

describe('Repository errors', () => {
Expand Down
Loading
Loading