mirror of
https://github.com/myronblair/kino-app
synced 2026-07-28 13:33:49 -05:00
Rebrand StreamHoard, strip Emergent.sh platform artifacts
- Remove .emergent/ state dir, tracked .gitconfig, @emergentbase/visual-edits dep + craco wiring, emergent.sh script/meta tags, PostHog telemetry snippet - Rename UI/docs branding Kino -> StreamHoard (README, DEPLOY.md, Navbar, Login, Register, AdminShowEpisodes, FastAPI title, health check response) - Rename docker-compose container/network names to streamhoard-* - Default admin email domain -> admin@streamhoard.local
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"env_image_name": "fastapi_react_mongo_shadcn_base_image_cloud_arm:release-17042026-1",
|
|
||||||
"job_id": "08d9a7a1-2a0c-4502-9939-fc5d643c04c4",
|
|
||||||
"created_at": "2026-07-23T22:57:32.873830+00:00Z"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[user]
|
|
||||||
email = github@emergent.sh
|
|
||||||
name = emergent-agent-e1
|
|
||||||
@@ -61,7 +61,6 @@ logs/
|
|||||||
|
|
||||||
# --- Test reports / agent runtime ---
|
# --- Test reports / agent runtime ---
|
||||||
test_reports/
|
test_reports/
|
||||||
.emergent/
|
|
||||||
tests/__pycache__/
|
tests/__pycache__/
|
||||||
|
|
||||||
# --- Misc ---
|
# --- Misc ---
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Kino — Deployment on a Proxmox LAMP host
|
# StreamHoard — Deployment on a Proxmox LAMP host
|
||||||
|
|
||||||
Kino is FastAPI + MongoDB + React. It runs happily alongside your existing
|
StreamHoard is FastAPI + MongoDB + React. It runs happily alongside your existing
|
||||||
LAMP stack (Apache/MySQL/PHP) because we run everything in Docker containers
|
LAMP stack (Apache/MySQL/PHP) because we run everything in Docker containers
|
||||||
on their own network. Apache keeps port 80/443, MySQL is untouched, Kino
|
on their own network. Apache keeps port 80/443, MySQL is untouched, StreamHoard
|
||||||
uses whatever port you pick.
|
uses whatever port you pick.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -54,7 +54,7 @@ Log in with the admin credentials from `.env`.
|
|||||||
|
|
||||||
## Running behind your existing Apache (optional, recommended)
|
## Running behind your existing Apache (optional, recommended)
|
||||||
|
|
||||||
If you already have Apache on port 80/443 with virtual hosts, add a Kino vhost
|
If you already have Apache on port 80/443 with virtual hosts, add a StreamHoard vhost
|
||||||
that reverse-proxies to the container. This gives you `https://kino.example.com`
|
that reverse-proxies to the container. This gives you `https://kino.example.com`
|
||||||
instead of `http://ip:8080`.
|
instead of `http://ip:8080`.
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ To reset everything: `docker compose down -v` (deletes the mongo-data volume).
|
|||||||
|
|
||||||
## Coexistence with your LAMP stack
|
## Coexistence with your LAMP stack
|
||||||
|
|
||||||
| Kino uses | LAMP uses | Conflict? |
|
| StreamHoard uses | LAMP uses | Conflict? |
|
||||||
| ------------------------ | ----------------------- | --------- |
|
| ------------------------ | ----------------------- | --------- |
|
||||||
| Port `KINO_HTTP_PORT` | Apache 80/443 | No — different ports |
|
| Port `KINO_HTTP_PORT` | Apache 80/443 | No — different ports |
|
||||||
| MongoDB (internal only) | MySQL | No — different DBs, Mongo not exposed to host |
|
| MongoDB (internal only) | MySQL | No — different DBs, Mongo not exposed to host |
|
||||||
@@ -144,16 +144,16 @@ same box.
|
|||||||
|
|
||||||
## Radarr integration
|
## Radarr integration
|
||||||
|
|
||||||
If Radarr is running on the same Proxmox host (native or container), point Kino
|
If Radarr is running on the same Proxmox host (native or container), point StreamHoard
|
||||||
at it via **Admin → Settings → Radarr**:
|
at it via **Admin → Settings → Radarr**:
|
||||||
|
|
||||||
- **URL**: `http://<host-ip>:7878` (use the host IP, not `localhost` — containers
|
- **URL**: `http://<host-ip>:7878` (use the host IP, not `localhost` — containers
|
||||||
can't reach the host's `localhost` unless you add `host.docker.internal`)
|
can't reach the host's `localhost` unless you add `host.docker.internal`)
|
||||||
- **API key**: from Radarr → Settings → General → Security
|
- **API key**: from Radarr → Settings → General → Security
|
||||||
|
|
||||||
**Important**: For Kino to actually play the imported movies, the path Radarr
|
**Important**: For StreamHoard to actually play the imported movies, the path Radarr
|
||||||
reports (e.g. `/movies/Inception (2010)/Inception.mkv`) must be **readable from
|
reports (e.g. `/movies/Inception (2010)/Inception.mkv`) must be **readable from
|
||||||
inside the Kino backend container**. Mount Radarr's media path into Kino:
|
inside the StreamHoard backend container**. Mount Radarr's media path into StreamHoard:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml — extend the backend volumes
|
# docker-compose.yml — extend the backend volumes
|
||||||
@@ -163,7 +163,7 @@ backend:
|
|||||||
- /mnt/radarr-movies:/mnt/radarr-movies:ro # <— add this
|
- /mnt/radarr-movies:/mnt/radarr-movies:ro # <— add this
|
||||||
```
|
```
|
||||||
|
|
||||||
Radarr's file paths must be identical inside both Radarr and Kino containers
|
Radarr's file paths must be identical inside both Radarr and StreamHoard containers
|
||||||
(easiest: match the paths exactly).
|
(easiest: match the paths exactly).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -172,14 +172,14 @@ Radarr's file paths must be identical inside both Radarr and Kino containers
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s http://localhost:${KINO_HTTP_PORT:-8080}/api/ | jq
|
curl -s http://localhost:${KINO_HTTP_PORT:-8080}/api/ | jq
|
||||||
# {"app":"Kino","status":"ok"}
|
# {"app":"StreamHoard","status":"ok"}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Firewalling
|
## Firewalling
|
||||||
|
|
||||||
Kino has no built-in rate limiting or IP allowlist. Since you confirmed
|
StreamHoard has no built-in rate limiting or IP allowlist. Since you confirmed
|
||||||
**internal network only**, keep it that way:
|
**internal network only**, keep it that way:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Kino — Personal Media Server (Netflix Clone)
|
# StreamHoard — Personal Media Server (Netflix Clone)
|
||||||
|
|
||||||
A self-hosted Netflix-style streaming app for movies you legally own.
|
A self-hosted Netflix-style streaming app for movies you legally own.
|
||||||
Built with FastAPI + React + MongoDB.
|
Built with FastAPI + React + MongoDB.
|
||||||
@@ -41,7 +41,7 @@ See **[DEPLOY.md](./DEPLOY.md)** for LAMP-coexistence, Apache reverse proxy, Rad
|
|||||||
*(Legal alternative to auto-downloading — auto-download of copyrighted material is not implemented.)*
|
*(Legal alternative to auto-downloading — auto-download of copyrighted material is not implemented.)*
|
||||||
|
|
||||||
## Default admin
|
## Default admin
|
||||||
- Email: `admin@kino.local`
|
- Email: `admin@streamhoard.local`
|
||||||
- Password: `kino-admin-2026`
|
- Password: `kino-admin-2026`
|
||||||
|
|
||||||
Change `ADMIN_EMAIL` / `ADMIN_PASSWORD` in `backend/.env` before first start
|
Change `ADMIN_EMAIL` / `ADMIN_PASSWORD` in `backend/.env` before first start
|
||||||
@@ -118,7 +118,7 @@ Movie media files should live on your Proxmox storage, not Git.
|
|||||||
# Login
|
# Login
|
||||||
curl -X POST $URL/api/auth/login \
|
curl -X POST $URL/api/auth/login \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"email":"admin@kino.local","password":"kino-admin-2026"}'
|
-d '{"email":"admin@streamhoard.local","password":"kino-admin-2026"}'
|
||||||
|
|
||||||
# List movies
|
# List movies
|
||||||
curl $URL/api/movies
|
curl $URL/api/movies
|
||||||
@@ -130,7 +130,7 @@ curl $URL/api/stream/<movie_id>?auth=<token>
|
|||||||
## Why no auto-download?
|
## Why no auto-download?
|
||||||
|
|
||||||
Auto-downloading copyrighted movies from the internet is illegal in most
|
Auto-downloading copyrighted movies from the internet is illegal in most
|
||||||
jurisdictions (DMCA, EU copyright directive, etc.). Kino is built as a
|
jurisdictions (DMCA, EU copyright directive, etc.). StreamHoard is built as a
|
||||||
**personal media server** for content you legally own or that is in the
|
**personal media server** for content you legally own or that is in the
|
||||||
public domain (Internet Archive, Blender Open Movies, etc.).
|
public domain (Internet Archive, Blender Open Movies, etc.).
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -57,7 +57,7 @@ for d in (VIDEOS_DIR, POSTERS_DIR, SUBS_DIR, HLS_DIR):
|
|||||||
|
|
||||||
CHUNK_SIZE = 1024 * 1024
|
CHUNK_SIZE = 1024 * 1024
|
||||||
|
|
||||||
app = FastAPI(title="Kino")
|
app = FastAPI(title="StreamHoard")
|
||||||
api = APIRouter(prefix="/api")
|
api = APIRouter(prefix="/api")
|
||||||
bearer = HTTPBearer(auto_error=False)
|
bearer = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ async def on_startup():
|
|||||||
await db.profiles.create_index("user_id")
|
await db.profiles.create_index("user_id")
|
||||||
await db.subtitles.create_index("movie_id")
|
await db.subtitles.create_index("movie_id")
|
||||||
|
|
||||||
admin_email = os.environ.get("ADMIN_EMAIL", "admin@kino.local")
|
admin_email = os.environ.get("ADMIN_EMAIL", "admin@streamhoard.local")
|
||||||
if not await db.users.find_one({"email": admin_email}):
|
if not await db.users.find_one({"email": admin_email}):
|
||||||
admin_user = {
|
admin_user = {
|
||||||
"id": str(uuid.uuid4()),
|
"id": str(uuid.uuid4()),
|
||||||
@@ -1482,7 +1482,7 @@ async def _auto_fetch_subtitle(content_type: str, content_id: str, tmdb_id: Opti
|
|||||||
# ============ HEALTH ============
|
# ============ HEALTH ============
|
||||||
@api.get("/")
|
@api.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return {"app": "Kino", "status": "ok"}
|
return {"app": "StreamHoard", "status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
app.include_router(api)
|
app.include_router(api)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Backend pytest suite for Kino Personal Media Server.
|
Backend pytest suite for StreamHoard Personal Media Server.
|
||||||
Tests auth, movies, watchlist, progress, requests, and stream auth.
|
Tests auth, movies, watchlist, progress, requests, and stream auth.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
@@ -8,8 +8,8 @@ import uuid
|
|||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
BASE_URL = os.environ.get("REACT_APP_BACKEND_URL", "https://streamhoard.preview.emergentagent.com").rstrip("/")
|
BASE_URL = os.environ.get("REACT_APP_BACKEND_URL", "http://localhost:8001").rstrip("/")
|
||||||
ADMIN_EMAIL = "admin@kino.local"
|
ADMIN_EMAIL = "admin@streamhoard.local"
|
||||||
ADMIN_PASSWORD = "kino-admin-2026"
|
ADMIN_PASSWORD = "kino-admin-2026"
|
||||||
|
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ def admin_token(api):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def member_token(api):
|
def member_token(api):
|
||||||
email = f"TEST_user_{uuid.uuid4().hex[:8]}@kino.local"
|
email = f"TEST_user_{uuid.uuid4().hex[:8]}@streamhoard.local"
|
||||||
r = api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "Test User"})
|
r = api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "Test User"})
|
||||||
assert r.status_code == 200, f"register failed: {r.text}"
|
assert r.status_code == 200, f"register failed: {r.text}"
|
||||||
data = r.json()
|
data = r.json()
|
||||||
@@ -73,7 +73,7 @@ class TestAuth:
|
|||||||
assert r.status_code == 401
|
assert r.status_code == 401
|
||||||
|
|
||||||
def test_register_and_me(self, api):
|
def test_register_and_me(self, api):
|
||||||
email = f"TEST_reg_{uuid.uuid4().hex[:8]}@kino.local"
|
email = f"TEST_reg_{uuid.uuid4().hex[:8]}@streamhoard.local"
|
||||||
r = api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "Reg User"})
|
r = api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "Reg User"})
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
d = r.json()
|
d = r.json()
|
||||||
@@ -86,7 +86,7 @@ class TestAuth:
|
|||||||
assert r2.json()["email"] == email.lower()
|
assert r2.json()["email"] == email.lower()
|
||||||
|
|
||||||
def test_register_duplicate(self, api):
|
def test_register_duplicate(self, api):
|
||||||
email = f"TEST_dup_{uuid.uuid4().hex[:8]}@kino.local"
|
email = f"TEST_dup_{uuid.uuid4().hex[:8]}@streamhoard.local"
|
||||||
api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "U"})
|
api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "U"})
|
||||||
r = api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "U"})
|
r = api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "U"})
|
||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import pytest
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
BASE_URL = os.environ.get("REACT_APP_BACKEND_URL", "").rstrip("/")
|
BASE_URL = os.environ.get("REACT_APP_BACKEND_URL", "").rstrip("/")
|
||||||
ADMIN_EMAIL = "admin@kino.local"
|
ADMIN_EMAIL = "admin@streamhoard.local"
|
||||||
ADMIN_PASSWORD = "kino-admin-2026"
|
ADMIN_PASSWORD = "kino-admin-2026"
|
||||||
|
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ def admin_headers(admin_token):
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def member(api):
|
def member(api):
|
||||||
"""Create a fresh member for profile-scoped tests."""
|
"""Create a fresh member for profile-scoped tests."""
|
||||||
email = f"TEST_p2_{uuid.uuid4().hex[:8]}@kino.local"
|
email = f"TEST_p2_{uuid.uuid4().hex[:8]}@streamhoard.local"
|
||||||
r = api.post(f"{BASE_URL}/api/auth/register",
|
r = api.post(f"{BASE_URL}/api/auth/register",
|
||||||
json={"email": email, "password": "pass1234", "name": "P2 User"})
|
json={"email": email, "password": "pass1234", "name": "P2 User"})
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
@@ -101,7 +101,7 @@ class TestProfiles:
|
|||||||
|
|
||||||
def test_cannot_delete_last_profile(self, api):
|
def test_cannot_delete_last_profile(self, api):
|
||||||
# fresh user with single profile
|
# fresh user with single profile
|
||||||
email = f"TEST_solo_{uuid.uuid4().hex[:6]}@kino.local"
|
email = f"TEST_solo_{uuid.uuid4().hex[:6]}@streamhoard.local"
|
||||||
d = api.post(f"{BASE_URL}/api/auth/register",
|
d = api.post(f"{BASE_URL}/api/auth/register",
|
||||||
json={"email": email, "password": "pass1234", "name": "Solo"}).json()
|
json={"email": email, "password": "pass1234", "name": "Solo"}).json()
|
||||||
h = {"Authorization": f"Bearer {d['access_token']}"}
|
h = {"Authorization": f"Bearer {d['access_token']}"}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import pytest
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
BASE_URL = os.environ["REACT_APP_BACKEND_URL"].rstrip("/")
|
BASE_URL = os.environ["REACT_APP_BACKEND_URL"].rstrip("/")
|
||||||
ADMIN_EMAIL = "admin@kino.local"
|
ADMIN_EMAIL = "admin@streamhoard.local"
|
||||||
ADMIN_PASSWORD = "kino-admin-2026"
|
ADMIN_PASSWORD = "kino-admin-2026"
|
||||||
QTEST_VIDEO = "/tmp/qtest.mp4"
|
QTEST_VIDEO = "/tmp/qtest.mp4"
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ def admin_token(api):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def member_token(api):
|
def member_token(api):
|
||||||
email = f"TEST_q_{uuid.uuid4().hex[:8]}@kino.local"
|
email = f"TEST_q_{uuid.uuid4().hex[:8]}@streamhoard.local"
|
||||||
r = api.post(f"{BASE_URL}/api/auth/register",
|
r = api.post(f"{BASE_URL}/api/auth/register",
|
||||||
json={"email": email, "password": "pass1234", "name": "QTest"},
|
json={"email": email, "password": "pass1234", "name": "QTest"},
|
||||||
headers={"Content-Type": "application/json"})
|
headers={"Content-Type": "application/json"})
|
||||||
|
|||||||
+13
-13
@@ -1,9 +1,9 @@
|
|||||||
# Kino — Docker Compose deployment
|
# StreamHoard — Docker Compose deployment
|
||||||
#
|
#
|
||||||
# Runs alongside existing LAMP stacks without conflicts:
|
# Runs alongside existing LAMP stacks without conflicts:
|
||||||
# - Apache keeps port 80/443
|
# - Apache keeps port 80/443
|
||||||
# - MySQL is untouched (Kino uses its own MongoDB in a container)
|
# - MySQL is untouched (StreamHoard uses its own MongoDB in a container)
|
||||||
# - Kino exposes a single configurable port (default 8080)
|
# - StreamHoard exposes a single configurable port (default 8080)
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# cp .env.compose.example .env
|
# cp .env.compose.example .env
|
||||||
@@ -14,19 +14,19 @@
|
|||||||
services:
|
services:
|
||||||
mongo:
|
mongo:
|
||||||
image: mongo:4.4
|
image: mongo:4.4
|
||||||
container_name: kino-mongo
|
container_name: streamhoard-mongo
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- mongo-data:/data/db
|
- mongo-data:/data/db
|
||||||
networks:
|
networks:
|
||||||
- kino
|
- streamhoard
|
||||||
# No ports exposed to host — only reachable inside kino network
|
# No ports exposed to host — only reachable inside streamhoard network
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: kino-backend
|
container_name: streamhoard-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongo
|
- mongo
|
||||||
@@ -37,7 +37,7 @@ services:
|
|||||||
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
|
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
|
||||||
JWT_ALG: HS256
|
JWT_ALG: HS256
|
||||||
JWT_EXPIRE_HOURS: ${JWT_EXPIRE_HOURS:-720}
|
JWT_EXPIRE_HOURS: ${JWT_EXPIRE_HOURS:-720}
|
||||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@kino.local}
|
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@streamhoard.local}
|
||||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?set ADMIN_PASSWORD in .env}
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?set ADMIN_PASSWORD in .env}
|
||||||
ADMIN_NAME: ${ADMIN_NAME:-Admin}
|
ADMIN_NAME: ${ADMIN_NAME:-Admin}
|
||||||
MEDIA_ROOT: /media
|
MEDIA_ROOT: /media
|
||||||
@@ -45,25 +45,25 @@ services:
|
|||||||
# Host media directory (movies, HLS, subtitles) — point at your NAS/ZFS mount
|
# Host media directory (movies, HLS, subtitles) — point at your NAS/ZFS mount
|
||||||
- ${MEDIA_ROOT_HOST:-./media}:/media
|
- ${MEDIA_ROOT_HOST:-./media}:/media
|
||||||
networks:
|
networks:
|
||||||
- kino
|
- streamhoard
|
||||||
# No direct host port — frontend container reverse-proxies /api → backend:8001
|
# No direct host port — frontend container reverse-proxies /api → backend:8001
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: kino-frontend
|
container_name: streamhoard-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
ports:
|
ports:
|
||||||
# Only Kino's external port. Change KINO_HTTP_PORT to avoid clashes with Apache.
|
# Only StreamHoard's external port. Change KINO_HTTP_PORT to avoid clashes with Apache.
|
||||||
- "${KINO_HTTP_PORT:-8080}:80"
|
- "${KINO_HTTP_PORT:-8080}:80"
|
||||||
networks:
|
networks:
|
||||||
- kino
|
- streamhoard
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
kino:
|
streamhoard:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
// Check if we're in development/preview mode (not production build)
|
|
||||||
// Craco sets NODE_ENV=development for start, NODE_ENV=production for build
|
|
||||||
const isDevServer = process.env.NODE_ENV !== "production";
|
|
||||||
|
|
||||||
// Environment variable overrides
|
// Environment variable overrides
|
||||||
const config = {
|
const config = {
|
||||||
enableHealthCheck: process.env.ENABLE_HEALTH_CHECK === "true",
|
enableHealthCheck: process.env.ENABLE_HEALTH_CHECK === "true",
|
||||||
@@ -81,20 +77,4 @@ webpackConfig.devServer = (devServerConfig) => {
|
|||||||
return devServerConfig;
|
return devServerConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wrap with visual edits (automatically adds babel plugin, dev server, and overlay in dev mode)
|
|
||||||
if (isDevServer) {
|
|
||||||
try {
|
|
||||||
const { withVisualEdits } = require("@emergentbase/visual-edits/craco");
|
|
||||||
webpackConfig = withVisualEdits(webpackConfig);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === 'MODULE_NOT_FOUND' && err.message.includes('@emergentbase/visual-edits/craco')) {
|
|
||||||
console.warn(
|
|
||||||
"[visual-edits] @emergentbase/visual-edits not installed — visual editing disabled."
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = webpackConfig;
|
module.exports = webpackConfig;
|
||||||
|
|||||||
@@ -76,7 +76,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@craco/craco": "^7.1.0",
|
"@craco/craco": "^7.1.0",
|
||||||
"@emergentbase/visual-edits": "https://assets.emergent.sh/npm/emergentbase-visual-edits-1.0.8.tgz",
|
|
||||||
"@eslint/js": "9.23.0",
|
"@eslint/js": "9.23.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "9.23.0",
|
"eslint": "9.23.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="A product of emergent.sh" />
|
<meta name="description" content="StreamHoard - self-hosted personal media server" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@600&display=swap" rel="stylesheet" />
|
||||||
@@ -21,9 +21,8 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>Emergent | Fullstack App</title>
|
<title>StreamHoard</title>
|
||||||
<script>window.addEventListener("error",function(e){if(e.error instanceof DOMException&&e.error.name==="DataCloneError"&&e.message&&e.message.includes("PerformanceServerTiming")){e.stopImmediatePropagation();e.preventDefault()}},true);</script>
|
<script>window.addEventListener("error",function(e){if(e.error instanceof DOMException&&e.error.name==="DataCloneError"&&e.message&&e.message.includes("PerformanceServerTiming")){e.stopImmediatePropagation();e.preventDefault()}},true);</script>
|
||||||
<script src="https://assets.emergent.sh/scripts/emergent-main.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
@@ -38,76 +37,5 @@
|
|||||||
To begin the development, run `npm start` or `yarn start`.
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
-->
|
-->
|
||||||
<script>
|
|
||||||
!(function (t, e) {
|
|
||||||
var o, n, p, r;
|
|
||||||
e.__SV ||
|
|
||||||
((window.posthog = e),
|
|
||||||
(e._i = []),
|
|
||||||
(e.init = function (i, s, a) {
|
|
||||||
function g(t, e) {
|
|
||||||
var o = e.split(".");
|
|
||||||
2 == o.length && ((t = t[o[0]]), (e = o[1])),
|
|
||||||
(t[e] = function () {
|
|
||||||
t.push(
|
|
||||||
[e].concat(
|
|
||||||
Array.prototype.slice.call(
|
|
||||||
arguments,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
((p = t.createElement("script")).type =
|
|
||||||
"text/javascript"),
|
|
||||||
(p.crossOrigin = "anonymous"),
|
|
||||||
(p.async = !0),
|
|
||||||
(p.src =
|
|
||||||
s.api_host.replace(
|
|
||||||
".i.posthog.com",
|
|
||||||
"-assets.i.posthog.com",
|
|
||||||
) + "/static/array.js"),
|
|
||||||
(r =
|
|
||||||
t.getElementsByTagName(
|
|
||||||
"script",
|
|
||||||
)[0]).parentNode.insertBefore(p, r);
|
|
||||||
var u = e;
|
|
||||||
for (
|
|
||||||
void 0 !== a ? (u = e[a] = []) : (a = "posthog"),
|
|
||||||
u.people = u.people || [],
|
|
||||||
u.toString = function (t) {
|
|
||||||
var e = "posthog";
|
|
||||||
return (
|
|
||||||
"posthog" !== a && (e += "." + a),
|
|
||||||
t || (e += " (stub)"),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
},
|
|
||||||
u.people.toString = function () {
|
|
||||||
return u.toString(1) + ".people (stub)";
|
|
||||||
},
|
|
||||||
o =
|
|
||||||
"init me ws ys ps bs capture je Di ks register register_once register_for_session unregister unregister_for_session Ps getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Es $s createPersonProfile Is opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing Ss debug xs getPageViewId captureTraceFeedback captureTraceMetric".split(
|
|
||||||
" ",
|
|
||||||
),
|
|
||||||
n = 0;
|
|
||||||
n < o.length;
|
|
||||||
n++
|
|
||||||
)
|
|
||||||
g(u, o[n]);
|
|
||||||
e._i.push([i, s, a]);
|
|
||||||
}),
|
|
||||||
(e.__SV = 1));
|
|
||||||
})(document, window.posthog || []);
|
|
||||||
posthog.init("phc_xAvL2Iq4tFmANRE7kzbKwaSqp1HJjN7x48s3vr0CMjs", {
|
|
||||||
api_host: "https://us.i.posthog.com",
|
|
||||||
person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well,
|
|
||||||
session_recording: {
|
|
||||||
recordCrossOriginIframes: true,
|
|
||||||
capturePerformance: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const Navbar = () => {
|
|||||||
<div className="px-6 md:px-12 py-4 flex items-center justify-between">
|
<div className="px-6 md:px-12 py-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-10">
|
<div className="flex items-center gap-10">
|
||||||
<Link to="/browse" className="flex items-center gap-2" data-testid="nav-logo">
|
<Link to="/browse" className="flex items-center gap-2" data-testid="nav-logo">
|
||||||
<span className="font-display text-2xl font-black tracking-tighter text-white">Kino</span>
|
<span className="font-display text-2xl font-black tracking-tighter text-white">StreamHoard</span>
|
||||||
<span className="text-[#D9381E] text-2xl leading-none">.</span>
|
<span className="text-[#D9381E] text-2xl leading-none">.</span>
|
||||||
</Link>
|
</Link>
|
||||||
{user && (
|
{user && (
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function AdminShowEpisodes() {
|
|||||||
const { data } = await api.post("/episodes/bulk-hide", { episode_ids: ids, hidden: false });
|
const { data } = await api.post("/episodes/bulk-hide", { episode_ids: ids, hidden: false });
|
||||||
msg = `${data.modified} unhidden`;
|
msg = `${data.modified} unhidden`;
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
if (!window.confirm(`Delete ${ids.length} episode(s) permanently? Files on disk are kept, but Kino entries and HLS output are removed.`)) return;
|
if (!window.confirm(`Delete ${ids.length} episode(s) permanently? Files on disk are kept, but StreamHoard entries and HLS output are removed.`)) return;
|
||||||
const { data } = await api.post("/episodes/bulk-delete", { episode_ids: ids });
|
const { data } = await api.post("/episodes/bulk-delete", { episode_ids: ids });
|
||||||
msg = `${data.deleted} deleted`;
|
msg = `${data.deleted} deleted`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function Login() {
|
|||||||
<div className="flex items-center justify-center px-6 md:px-12 py-12">
|
<div className="flex items-center justify-center px-6 md:px-12 py-12">
|
||||||
<form onSubmit={onSubmit} className="w-full max-w-sm fade-up" data-testid="login-form">
|
<form onSubmit={onSubmit} className="w-full max-w-sm fade-up" data-testid="login-form">
|
||||||
<Link to="/" className="block mb-12" data-testid="login-logo">
|
<Link to="/" className="block mb-12" data-testid="login-logo">
|
||||||
<span className="font-display text-3xl font-black tracking-tighter text-white">Kino</span>
|
<span className="font-display text-3xl font-black tracking-tighter text-white">StreamHoard</span>
|
||||||
<span className="text-[#D9381E] text-3xl">.</span>
|
<span className="text-[#D9381E] text-3xl">.</span>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-display text-3xl font-bold tracking-tight text-white">Sign in</h2>
|
<h2 className="font-display text-3xl font-bold tracking-tight text-white">Sign in</h2>
|
||||||
@@ -93,7 +93,7 @@ export default function Login() {
|
|||||||
|
|
||||||
<div className="mt-8 p-4 border border-[#222] text-xs text-[#8A8A8A]" data-testid="login-demo-credentials">
|
<div className="mt-8 p-4 border border-[#222] text-xs text-[#8A8A8A]" data-testid="login-demo-credentials">
|
||||||
<span className="text-[10px] uppercase tracking-[0.3em] text-[#D9381E] block mb-2">Demo Admin</span>
|
<span className="text-[10px] uppercase tracking-[0.3em] text-[#D9381E] block mb-2">Demo Admin</span>
|
||||||
admin@kino.local / kino-admin-2026
|
admin@streamhoard.local / kino-admin-2026
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function Register() {
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await register(email, password, name);
|
await register(email, password, name);
|
||||||
toast.success("Welcome to Kino");
|
toast.success("Welcome to StreamHoard");
|
||||||
nav("/browse", { replace: true });
|
nav("/browse", { replace: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.response?.data?.detail || "Could not register");
|
toast.error(err.response?.data?.detail || "Could not register");
|
||||||
@@ -49,7 +49,7 @@ export default function Register() {
|
|||||||
<div className="flex items-center justify-center px-6 md:px-12 py-12">
|
<div className="flex items-center justify-center px-6 md:px-12 py-12">
|
||||||
<form onSubmit={onSubmit} className="w-full max-w-sm fade-up" data-testid="register-form">
|
<form onSubmit={onSubmit} className="w-full max-w-sm fade-up" data-testid="register-form">
|
||||||
<Link to="/" className="block mb-12">
|
<Link to="/" className="block mb-12">
|
||||||
<span className="font-display text-3xl font-black tracking-tighter text-white">Kino</span>
|
<span className="font-display text-3xl font-black tracking-tighter text-white">StreamHoard</span>
|
||||||
<span className="text-[#D9381E] text-3xl">.</span>
|
<span className="text-[#D9381E] text-3xl">.</span>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-display text-3xl font-bold tracking-tight text-white">Create account</h2>
|
<h2 className="font-display text-3xl font-bold tracking-tight text-white">Create account</h2>
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Kino — one-line installer
|
# StreamHoard — one-line installer
|
||||||
#
|
#
|
||||||
# Usage (interactive):
|
# Usage (interactive):
|
||||||
# curl -fsSL https://raw.githubusercontent.com/YOU/kino-media-server/main/install.sh | sudo bash
|
# curl -fsSL https://raw.githubusercontent.com/YOU/kino-media-server/main/install.sh | sudo bash
|
||||||
@@ -25,7 +25,7 @@ KINO_DIR="${KINO_DIR:-/opt/kino}"
|
|||||||
KINO_HTTP_PORT="${KINO_HTTP_PORT:-8080}"
|
KINO_HTTP_PORT="${KINO_HTTP_PORT:-8080}"
|
||||||
MEDIA_ROOT_HOST="${MEDIA_ROOT_HOST:-${KINO_DIR}/media}"
|
MEDIA_ROOT_HOST="${MEDIA_ROOT_HOST:-${KINO_DIR}/media}"
|
||||||
DB_NAME="${DB_NAME:-kino}"
|
DB_NAME="${DB_NAME:-kino}"
|
||||||
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@kino.local}"
|
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@streamhoard.local}"
|
||||||
ADMIN_NAME="${ADMIN_NAME:-Admin}"
|
ADMIN_NAME="${ADMIN_NAME:-Admin}"
|
||||||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
|
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
|
||||||
JWT_SECRET="${JWT_SECRET:-}"
|
JWT_SECRET="${JWT_SECRET:-}"
|
||||||
@@ -172,7 +172,7 @@ fi
|
|||||||
# ---------- Done ----------
|
# ---------- Done ----------
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
$(c_grn '━━━ Kino is up ━━━')
|
$(c_grn '━━━ StreamHoard is up ━━━')
|
||||||
|
|
||||||
URL : $(c_cyn "$URL")
|
URL : $(c_cyn "$URL")
|
||||||
Admin : $(c_cyn "$ADMIN_EMAIL")
|
Admin : $(c_cyn "$ADMIN_EMAIL")
|
||||||
|
|||||||
Reference in New Issue
Block a user