Merge: rebrand to Siftlode
This commit is contained in:
commit
4826a5811f
24 changed files with 71 additions and 71 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
# Subfeed
|
# Siftlode
|
||||||
|
|
||||||
Self-hosted, multi-user web app for browsing **your own YouTube subscriptions** the way you
|
Self-hosted, multi-user web app for browsing **your own YouTube subscriptions** the way you
|
||||||
actually want: precise filtering and sorting (by language, topic, length, age, watch state…),
|
actually want: precise filtering and sorting (by language, topic, length, age, watch state…),
|
||||||
|
|
@ -66,7 +66,7 @@ server) does **not** require re-fetching from YouTube. Copy your `.env` (keep th
|
||||||
`TOKEN_ENCRYPTION_KEY` and Google client so stored tokens stay valid), then:
|
`TOKEN_ENCRYPTION_KEY` and Google client so stored tokens stay valid), then:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./scripts/backup.sh # -> backups/subfeed-<timestamp>.dump (Windows: scripts\backup.ps1)
|
./scripts/backup.sh # -> backups/siftlode-<timestamp>.dump (Windows: scripts\backup.ps1)
|
||||||
./scripts/restore.sh backups/<file> # on the new host after `docker compose up`
|
./scripts/restore.sh backups/<file> # on the new host after `docker compose up`
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ _DEFAULT_SECRET_KEY = "change-me-session-key"
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||||
|
|
||||||
app_name: str = "Subfeed"
|
app_name: str = "Siftlode"
|
||||||
|
|
||||||
database_url: str = "postgresql+psycopg://subfeed:subfeed@db:5432/subfeed"
|
database_url: str = "postgresql+psycopg://subfeed:subfeed@db:5432/subfeed"
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ class Settings(BaseSettings):
|
||||||
smtp_port: int = 587
|
smtp_port: int = 587
|
||||||
smtp_user: str = ""
|
smtp_user: str = ""
|
||||||
smtp_password: str = ""
|
smtp_password: str = ""
|
||||||
smtp_from: str = "" # e.g. "Subfeed <addr@gmail.com>"; falls back to smtp_user
|
smtp_from: str = "" # e.g. "Siftlode <addr@gmail.com>"; falls back to smtp_user
|
||||||
|
|
||||||
# --- Sync / YouTube Data API ---
|
# --- Sync / YouTube Data API ---
|
||||||
# Optional API key for public reads (channels/videos/playlistItems). When set it is
|
# Optional API key for public reads (channels/videos/playlistItems). When set it is
|
||||||
|
|
|
||||||
|
|
@ -56,20 +56,20 @@ def send_access_approved(email: str) -> bool:
|
||||||
admin = _admin_contact()
|
admin = _admin_contact()
|
||||||
body = (
|
body = (
|
||||||
"Hi,\n\n"
|
"Hi,\n\n"
|
||||||
"Your request to use Subfeed has been approved — welcome aboard.\n\n"
|
"Your request to use Siftlode has been approved — welcome aboard.\n\n"
|
||||||
"Just open Subfeed and sign in with this Google account, and your YouTube\n"
|
"Just open Siftlode and sign in with this Google account, and your YouTube\n"
|
||||||
"subscriptions feed will be ready.\n\n"
|
"subscriptions feed will be ready.\n\n"
|
||||||
"If you weren't expecting this, you can ignore the message.\n\n"
|
"If you weren't expecting this, you can ignore the message.\n\n"
|
||||||
"— Subfeed"
|
"— Siftlode"
|
||||||
+ (f"\n\nQuestions? Just reply to this email ({admin})." if admin else "")
|
+ (f"\n\nQuestions? Just reply to this email ({admin})." if admin else "")
|
||||||
)
|
)
|
||||||
return _send([email], "You're in — your Subfeed access is approved", body, reply_to=admin)
|
return _send([email], "You're in — your Siftlode access is approved", body, reply_to=admin)
|
||||||
|
|
||||||
|
|
||||||
def send_admin_new_request(admins: list[str], requester: str) -> bool:
|
def send_admin_new_request(admins: list[str], requester: str) -> bool:
|
||||||
body = (
|
body = (
|
||||||
f"{requester} just requested access to Subfeed.\n\n"
|
f"{requester} just requested access to Siftlode.\n\n"
|
||||||
"Open Subfeed → Settings → Account → Access requests to approve or deny it.\n\n"
|
"Open Siftlode → Settings → Account → Access requests to approve or deny it.\n\n"
|
||||||
"(Reply to this email to reach the requester directly.)\n"
|
"(Reply to this email to reach the requester directly.)\n"
|
||||||
)
|
)
|
||||||
return _send(admins, f"Subfeed access request from {requester}", body, reply_to=requester)
|
return _send(admins, f"Siftlode access request from {requester}", body, reply_to=requester)
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,12 @@ from app.scheduler import shutdown_scheduler, start_scheduler
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
log.info("Subfeed starting up")
|
log.info("Siftlode starting up")
|
||||||
start_scheduler()
|
start_scheduler()
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
log.info("Subfeed shutting down")
|
log.info("Siftlode shutting down")
|
||||||
shutdown_scheduler()
|
shutdown_scheduler()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<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" />
|
||||||
<title>Subfeed</title>
|
<title>Siftlode</title>
|
||||||
<style>
|
<style>
|
||||||
:root { color-scheme: dark; }
|
:root { color-scheme: dark; }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ RSS_URL = "https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
|
||||||
def fetch_channel_feed(channel_id: str) -> list[dict]:
|
def fetch_channel_feed(channel_id: str) -> list[dict]:
|
||||||
url = RSS_URL.format(channel_id=channel_id)
|
url = RSS_URL.format(channel_id=channel_id)
|
||||||
try:
|
try:
|
||||||
resp = httpx.get(url, timeout=20.0, headers={"User-Agent": "Subfeed/1.0"})
|
resp = httpx.get(url, timeout=20.0, headers={"User-Agent": "Siftlode/1.0"})
|
||||||
except httpx.HTTPError:
|
except httpx.HTTPError:
|
||||||
return []
|
return []
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,5 @@ set -e
|
||||||
echo "Applying database migrations..."
|
echo "Applying database migrations..."
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
|
|
||||||
echo "Starting Subfeed API..."
|
echo "Starting Siftlode API..."
|
||||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --log-config log_config.json
|
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --log-config log_config.json
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# Deploying Subfeed to the public VPS (the VPS)
|
# Deploying Siftlode to the public VPS (the VPS)
|
||||||
|
|
||||||
The public test instance runs on the the VPS VPS at **https://subfeed.b1fr0st.eu**, behind
|
The public test instance runs on the the VPS VPS at **https://siftlode.b1fr0st.eu**, behind
|
||||||
Caddy (which terminates TLS and reverse-proxies to the app on `127.0.0.1:8080`).
|
Caddy (which terminates TLS and reverse-proxies to the app on `127.0.0.1:8080`).
|
||||||
|
|
||||||
## Layout on the VPS
|
## Layout on the VPS
|
||||||
|
|
||||||
```
|
```
|
||||||
/srv/subfeed/
|
/srv/siftlode/
|
||||||
src/ git clone of this repo (build context + compose file)
|
src/ git clone of this repo (build context + compose file)
|
||||||
.env secrets, mode 0600 — NEVER committed
|
.env secrets, mode 0600 — NEVER committed
|
||||||
```
|
```
|
||||||
|
|
@ -17,16 +17,16 @@ the small VPS.
|
||||||
|
|
||||||
## First-time setup
|
## First-time setup
|
||||||
|
|
||||||
1. DNS `A`/`AAAA` for `subfeed` → the VPS (done via the your DNS provider CLI).
|
1. DNS `A`/`AAAA` for `siftlode` → the VPS (done via the your DNS provider CLI).
|
||||||
2. Caddy vhost block `subfeed.b1fr0st.eu { reverse_proxy 127.0.0.1:8080 }` (+ the shared
|
2. Caddy vhost block `siftlode.b1fr0st.eu { reverse_proxy 127.0.0.1:8080 }` (+ the shared
|
||||||
security-headers snippet), then reload Caddy.
|
security-headers snippet), then reload Caddy.
|
||||||
3. `git clone` this repo to `/srv/subfeed/src`.
|
3. `git clone` this repo to `/srv/siftlode/src`.
|
||||||
4. Create `/srv/subfeed/.env` (mode 0600) with the required keys — see
|
4. Create `/srv/siftlode/.env` (mode 0600) with the required keys — see
|
||||||
[`.env.example`](../.env.example). Generate the secrets:
|
[`.env.example`](../.env.example). Generate the secrets:
|
||||||
- `SECRET_KEY`: `python -c "import secrets; print(secrets.token_urlsafe(48))"`
|
- `SECRET_KEY`: `python -c "import secrets; print(secrets.token_urlsafe(48))"`
|
||||||
- `TOKEN_ENCRYPTION_KEY`: `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`
|
- `TOKEN_ENCRYPTION_KEY`: `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`
|
||||||
- `POSTGRES_PASSWORD`: any long random string.
|
- `POSTGRES_PASSWORD`: any long random string.
|
||||||
- `OAUTH_REDIRECT_URL=https://subfeed.b1fr0st.eu/auth/callback`
|
- `OAUTH_REDIRECT_URL=https://siftlode.b1fr0st.eu/auth/callback`
|
||||||
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` / `YOUTUBE_API_KEY` from Google Cloud Console.
|
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` / `YOUTUBE_API_KEY` from Google Cloud Console.
|
||||||
- `ALLOWED_EMAILS` / `ADMIN_EMAILS` = the invited Google accounts.
|
- `ALLOWED_EMAILS` / `ADMIN_EMAILS` = the invited Google accounts.
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ the small VPS.
|
||||||
## Rolling out a new version
|
## Rolling out a new version
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/srv/subfeed/src/deploy/deploy.sh
|
/srv/siftlode/src/deploy/deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
It pulls `main`, rebuilds the image on the host, and restarts the stack (migrations run
|
It pulls `main`, rebuilds the image on the host, and restarts the stack (migrations run
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
# Roll out the latest main branch to the public the VPS instance.
|
# Roll out the latest main branch to the public the VPS instance.
|
||||||
#
|
#
|
||||||
# Layout on the VPS:
|
# Layout on the VPS:
|
||||||
# /srv/subfeed/src git clone of this repo (build context + compose file)
|
# /srv/siftlode/src git clone of this repo (build context + compose file)
|
||||||
# /srv/subfeed/.env secrets, mode 0600 (never in git)
|
# /srv/siftlode/.env secrets, mode 0600 (never in git)
|
||||||
#
|
#
|
||||||
# Build runs on the host Docker daemon, so it isn't constrained by the CI runner's memory.
|
# Build runs on the host Docker daemon, so it isn't constrained by the CI runner's memory.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT=/srv/subfeed
|
ROOT=/srv/siftlode
|
||||||
cd "$ROOT/src"
|
cd "$ROOT/src"
|
||||||
|
|
||||||
git fetch --quiet origin
|
git fetch --quiet origin
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@
|
||||||
# has a hard memory/CPU cap so the stack can't exhaust the small VPS.
|
# has a hard memory/CPU cap so the stack can't exhaust the small VPS.
|
||||||
#
|
#
|
||||||
# Secrets live OUTSIDE the repo, in an env file on the host. Deploy with:
|
# Secrets live OUTSIDE the repo, in an env file on the host. Deploy with:
|
||||||
# docker compose -f docker-compose.prod.yml --env-file /srv/subfeed/.env up -d --build
|
# docker compose -f docker-compose.prod.yml --env-file /srv/siftlode/.env up -d --build
|
||||||
#
|
#
|
||||||
# Required keys in that env file: POSTGRES_PASSWORD, SECRET_KEY (>=32 chars),
|
# Required keys in that env file: POSTGRES_PASSWORD, SECRET_KEY (>=32 chars),
|
||||||
# TOKEN_ENCRYPTION_KEY (Fernet), GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
|
# TOKEN_ENCRYPTION_KEY (Fernet), GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
|
||||||
# OAUTH_REDIRECT_URL=https://subfeed.b1fr0st.eu/auth/callback, ALLOWED_EMAILS, ADMIN_EMAILS.
|
# OAUTH_REDIRECT_URL=https://siftlode.b1fr0st.eu/auth/callback, ALLOWED_EMAILS, ADMIN_EMAILS.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
|
|
@ -37,9 +37,9 @@ services:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: subfeed-api:local
|
image: siftlode-api:local
|
||||||
env_file:
|
env_file:
|
||||||
- /srv/subfeed/.env
|
- /srv/siftlode/.env
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-subfeed}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-subfeed}
|
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-subfeed}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-subfeed}
|
||||||
# The public instance owns the background scheduler (single writer).
|
# The public instance owns the background scheduler (single writer).
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Server (Proxmox LXC) deployment of the full Subfeed stack.
|
# Server (Proxmox LXC) deployment of the full Siftlode stack.
|
||||||
#
|
#
|
||||||
# This is the "central" instance: it owns the single shared Postgres database and
|
# This is the "central" instance: it owns the single shared Postgres database and
|
||||||
# runs the background scheduler (RSS poll, enrichment, recent/deep backfill) 24/7.
|
# runs the background scheduler (RSS poll, enrichment, recent/deep backfill) 24/7.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<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" />
|
||||||
<title>Subfeed</title>
|
<title>Siftlode</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "subfeed-frontend",
|
"name": "siftlode-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export default function Header({
|
||||||
className="text-xl font-bold tracking-tight select-none"
|
className="text-xl font-bold tracking-tight select-none"
|
||||||
title="Feed"
|
title="Feed"
|
||||||
>
|
>
|
||||||
Sub<span className="text-accent">feed</span>
|
Sift<span className="text-accent">lode</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<SyncStatus />
|
<SyncStatus />
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default function Login() {
|
||||||
<div className="min-h-screen grid place-items-center bg-bg text-fg">
|
<div className="min-h-screen grid place-items-center bg-bg text-fg">
|
||||||
<div className="glass w-[min(92vw,420px)] rounded-2xl p-10 text-center">
|
<div className="glass w-[min(92vw,420px)] rounded-2xl p-10 text-center">
|
||||||
<div className="text-3xl font-bold tracking-tight">
|
<div className="text-3xl font-bold tracking-tight">
|
||||||
Sub<span className="text-accent">feed</span>
|
Sift<span className="text-accent">lode</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted my-5 leading-relaxed">
|
<p className="text-muted my-5 leading-relaxed">
|
||||||
A self-hosted, filterable feed of your YouTube subscriptions — sort, tag, and
|
A self-hosted, filterable feed of your YouTube subscriptions — sort, tag, and
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ function ConsentHeadsUp() {
|
||||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-amber-400" />
|
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-amber-400" />
|
||||||
<p className="text-xs leading-relaxed text-fg/80">
|
<p className="text-xs leading-relaxed text-fg/80">
|
||||||
Google will show a <span className="font-medium">"Google hasn't verified this app"</span>{" "}
|
Google will show a <span className="font-medium">"Google hasn't verified this app"</span>{" "}
|
||||||
screen — that's expected, because Subfeed hasn't gone through Google's full review.
|
screen — that's expected, because Siftlode hasn't gone through Google's full review.
|
||||||
It's safe to continue: click <span className="font-medium">Advanced</span> →{" "}
|
It's safe to continue: click <span className="font-medium">Advanced</span> →{" "}
|
||||||
<span className="font-medium">Go to Subfeed</span>. You can revoke access anytime from
|
<span className="font-medium">Go to Siftlode</span>. You can revoke access anytime from
|
||||||
your{" "}
|
your{" "}
|
||||||
<a
|
<a
|
||||||
href="https://myaccount.google.com/permissions"
|
href="https://myaccount.google.com/permissions"
|
||||||
|
|
@ -90,7 +90,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-semibold">Connect your YouTube subscriptions</h2>
|
<h2 className="text-lg font-semibold">Connect your YouTube subscriptions</h2>
|
||||||
<p className="text-sm text-muted leading-relaxed mt-2 mb-4">
|
<p className="text-sm text-muted leading-relaxed mt-2 mb-4">
|
||||||
You're signed in. To build your feed, Subfeed needs{" "}
|
You're signed in. To build your feed, Siftlode needs{" "}
|
||||||
<span className="text-fg/90">read-only</span> access to the channels you're
|
<span className="text-fg/90">read-only</span> access to the channels you're
|
||||||
subscribed to on YouTube. It never posts, deletes, or changes anything with this.
|
subscribed to on YouTube. It never posts, deletes, or changes anything with this.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -119,7 +119,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-semibold">You're connected</h2>
|
<h2 className="text-lg font-semibold">You're connected</h2>
|
||||||
<p className="text-sm text-muted leading-relaxed mt-2 mb-4">
|
<p className="text-sm text-muted leading-relaxed mt-2 mb-4">
|
||||||
Your feed is ready. Optionally, you can let Subfeed{" "}
|
Your feed is ready. Optionally, you can let Siftlode{" "}
|
||||||
<span className="text-fg/90">unsubscribe</span> from channels on YouTube for you —
|
<span className="text-fg/90">unsubscribe</span> from channels on YouTube for you —
|
||||||
this needs an extra write permission. Skip it to stay read-only; you can always
|
this needs an extra write permission. Skip it to stay read-only; you can always
|
||||||
enable it later in Settings.
|
enable it later in Settings.
|
||||||
|
|
|
||||||
|
|
@ -492,7 +492,7 @@ function AccessRow({
|
||||||
Granted
|
Granted
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip hint="Redirects to Google to grant the permission. Google shows an 'unverified app' notice — that's expected; click Advanced → Go to Subfeed.">
|
<Tooltip hint="Redirects to Google to grant the permission. Google shows an 'unverified app' notice — that's expected; click Advanced → Go to Siftlode.">
|
||||||
<button
|
<button
|
||||||
onClick={onEnable}
|
onClick={onEnable}
|
||||||
className="shrink-0 glass-card glass-hover px-3 py-1.5 rounded-xl text-sm transition"
|
className="shrink-0 glass-card glass-hover px-3 py-1.5 rounded-xl text-sm transition"
|
||||||
|
|
@ -532,8 +532,8 @@ function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
|
||||||
<AccessRow
|
<AccessRow
|
||||||
title="Read subscriptions (your feed)"
|
title="Read subscriptions (your feed)"
|
||||||
granted={me.can_read}
|
granted={me.can_read}
|
||||||
grantedHint="Granted. Subfeed reads the channels you follow to build your feed. It never changes your YouTube account with this."
|
grantedHint="Granted. Siftlode reads the channels you follow to build your feed. It never changes your YouTube account with this."
|
||||||
enableHint="Read-only access to your subscriptions — required to build your feed. Subfeed can't modify anything with it."
|
enableHint="Read-only access to your subscriptions — required to build your feed. Siftlode can't modify anything with it."
|
||||||
onEnable={() => {
|
onEnable={() => {
|
||||||
window.location.href = "/auth/upgrade?access=read";
|
window.location.href = "/auth/upgrade?access=read";
|
||||||
}}
|
}}
|
||||||
|
|
@ -541,8 +541,8 @@ function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
|
||||||
<AccessRow
|
<AccessRow
|
||||||
title="Unsubscribe (write)"
|
title="Unsubscribe (write)"
|
||||||
granted={me.can_write}
|
granted={me.can_write}
|
||||||
grantedHint="Granted. You can unsubscribe from channels on YouTube from inside Subfeed. It only writes when you ask it to."
|
grantedHint="Granted. You can unsubscribe from channels on YouTube from inside Siftlode. It only writes when you ask it to."
|
||||||
enableHint="Lets Subfeed unsubscribe from channels on YouTube for you. Optional — skip it to stay read-only."
|
enableHint="Lets Siftlode unsubscribe from channels on YouTube for you. Optional — skip it to stay read-only."
|
||||||
onEnable={() => {
|
onEnable={() => {
|
||||||
window.location.href = "/auth/upgrade?access=write";
|
window.location.href = "/auth/upgrade?access=write";
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export default function LegalLayout({
|
||||||
<div className="min-h-screen bg-bg text-fg">
|
<div className="min-h-screen bg-bg text-fg">
|
||||||
<div className="mx-auto w-[min(92vw,720px)] py-12">
|
<div className="mx-auto w-[min(92vw,720px)] py-12">
|
||||||
<a href="/" className="inline-block text-2xl font-bold tracking-tight mb-8">
|
<a href="/" className="inline-block text-2xl font-bold tracking-tight mb-8">
|
||||||
Sub<span className="text-accent">feed</span>
|
Sift<span className="text-accent">lode</span>
|
||||||
</a>
|
</a>
|
||||||
<div className="glass rounded-2xl p-8">
|
<div className="glass rounded-2xl p-8">
|
||||||
<h1 className="text-2xl font-semibold">{title}</h1>
|
<h1 className="text-2xl font-semibold">{title}</h1>
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ export default function PrivacyPolicy() {
|
||||||
return (
|
return (
|
||||||
<LegalLayout title="Privacy Policy">
|
<LegalLayout title="Privacy Policy">
|
||||||
<p>
|
<p>
|
||||||
Subfeed is a personal, non-commercial project operated by an individual in Hungary
|
Siftlode is a personal, non-commercial project operated by an individual in Hungary
|
||||||
("we", "us"). It lets you view and organize your own YouTube subscriptions as a
|
("we", "us"). It lets you view and organize your own YouTube subscriptions as a
|
||||||
filterable feed. This policy explains what data Subfeed accesses, how it is used, and
|
filterable feed. This policy explains what data Siftlode accesses, how it is used, and
|
||||||
the choices you have. By using Subfeed you agree to this policy.
|
the choices you have. By using Siftlode you agree to this policy.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<H2>What we access</H2>
|
<H2>What we access</H2>
|
||||||
<p>Subfeed only accesses data you explicitly authorize, in two separate steps:</p>
|
<p>Siftlode only accesses data you explicitly authorize, in two separate steps:</p>
|
||||||
<ul className="list-disc pl-5 space-y-1">
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
<li>
|
<li>
|
||||||
<span className="text-fg font-medium">Sign-in (always):</span> your basic Google
|
<span className="text-fg font-medium">Sign-in (always):</span> your basic Google
|
||||||
|
|
@ -31,13 +31,13 @@ export default function PrivacyPolicy() {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
You can use Subfeed with sign-in alone, and grant or decline each YouTube permission
|
You can use Siftlode with sign-in alone, and grant or decline each YouTube permission
|
||||||
independently.
|
independently.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<H2>How we use it</H2>
|
<H2>How we use it</H2>
|
||||||
<p>
|
<p>
|
||||||
Your data is used exclusively to provide Subfeed's features to you: building and
|
Your data is used exclusively to provide Siftlode's features to you: building and
|
||||||
displaying your personalized subscription feed, and carrying out actions (such as
|
displaying your personalized subscription feed, and carrying out actions (such as
|
||||||
unsubscribing) that you initiate. We do not use it for advertising, profiling, or any
|
unsubscribing) that you initiate. We do not use it for advertising, profiling, or any
|
||||||
purpose unrelated to the app.
|
purpose unrelated to the app.
|
||||||
|
|
@ -45,7 +45,7 @@ export default function PrivacyPolicy() {
|
||||||
|
|
||||||
<H2>Limited Use disclosure</H2>
|
<H2>Limited Use disclosure</H2>
|
||||||
<p>
|
<p>
|
||||||
Subfeed's use and transfer of information received from Google APIs to any other app
|
Siftlode's use and transfer of information received from Google APIs to any other app
|
||||||
will adhere to the{" "}
|
will adhere to the{" "}
|
||||||
<LegalLink href="https://developers.google.com/terms/api-services-user-data-policy">
|
<LegalLink href="https://developers.google.com/terms/api-services-user-data-policy">
|
||||||
Google API Services User Data Policy
|
Google API Services User Data Policy
|
||||||
|
|
@ -65,7 +65,7 @@ export default function PrivacyPolicy() {
|
||||||
|
|
||||||
<H2>Retention & deletion</H2>
|
<H2>Retention & deletion</H2>
|
||||||
<p>
|
<p>
|
||||||
We keep your data while your account is active. You can revoke Subfeed's access to your
|
We keep your data while your account is active. You can revoke Siftlode's access to your
|
||||||
Google data at any time at{" "}
|
Google data at any time at{" "}
|
||||||
<LegalLink href="https://myaccount.google.com/permissions">
|
<LegalLink href="https://myaccount.google.com/permissions">
|
||||||
myaccount.google.com/permissions
|
myaccount.google.com/permissions
|
||||||
|
|
@ -80,13 +80,13 @@ export default function PrivacyPolicy() {
|
||||||
|
|
||||||
<H2>Cookies</H2>
|
<H2>Cookies</H2>
|
||||||
<p>
|
<p>
|
||||||
Subfeed sets a single, signed session cookie to keep you logged in. It is not used for
|
Siftlode sets a single, signed session cookie to keep you logged in. It is not used for
|
||||||
tracking or advertising, and there are no third-party analytics cookies.
|
tracking or advertising, and there are no third-party analytics cookies.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<H2>YouTube & Google</H2>
|
<H2>YouTube & Google</H2>
|
||||||
<p>
|
<p>
|
||||||
Subfeed uses YouTube API Services. By using Subfeed you also agree to the{" "}
|
Siftlode uses YouTube API Services. By using Siftlode you also agree to the{" "}
|
||||||
<LegalLink href="https://www.youtube.com/t/terms">YouTube Terms of Service</LegalLink>,
|
<LegalLink href="https://www.youtube.com/t/terms">YouTube Terms of Service</LegalLink>,
|
||||||
and your use of Google data is subject to the{" "}
|
and your use of Google data is subject to the{" "}
|
||||||
<LegalLink href="https://policies.google.com/privacy">Google Privacy Policy</LegalLink>.
|
<LegalLink href="https://policies.google.com/privacy">Google Privacy Policy</LegalLink>.
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,21 @@ export default function Terms() {
|
||||||
return (
|
return (
|
||||||
<LegalLayout title="Terms of Service">
|
<LegalLayout title="Terms of Service">
|
||||||
<p>
|
<p>
|
||||||
Subfeed is a personal, non-commercial project operated by an individual in Hungary. It
|
Siftlode is a personal, non-commercial project operated by an individual in Hungary. It
|
||||||
provides a filterable view of your own YouTube subscriptions. By accessing or using
|
provides a filterable view of your own YouTube subscriptions. By accessing or using
|
||||||
Subfeed, you agree to these Terms. If you do not agree, do not use the service.
|
Siftlode, you agree to these Terms. If you do not agree, do not use the service.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<H2>The service</H2>
|
<H2>The service</H2>
|
||||||
<p>
|
<p>
|
||||||
Subfeed is offered on an invite-only basis, free of charge, and "as is", without
|
Siftlode is offered on an invite-only basis, free of charge, and "as is", without
|
||||||
warranties of any kind. It is a hobby project: features may change, and the service may
|
warranties of any kind. It is a hobby project: features may change, and the service may
|
||||||
be slowed, interrupted, or discontinued at any time without notice.
|
be slowed, interrupted, or discontinued at any time without notice.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<H2>Your account & acceptable use</H2>
|
<H2>Your account & acceptable use</H2>
|
||||||
<p>
|
<p>
|
||||||
You sign in with your Google account and may only access Subfeed with an email that has
|
You sign in with your Google account and may only access Siftlode with an email that has
|
||||||
been invited. You agree to use the service lawfully, not to abuse, disrupt, probe, or
|
been invited. You agree to use the service lawfully, not to abuse, disrupt, probe, or
|
||||||
overload it, and not to attempt to access other users' data. We may suspend or revoke
|
overload it, and not to attempt to access other users' data. We may suspend or revoke
|
||||||
access at our discretion, for example in case of misuse.
|
access at our discretion, for example in case of misuse.
|
||||||
|
|
@ -26,7 +26,7 @@ export default function Terms() {
|
||||||
|
|
||||||
<H2>Third-party services</H2>
|
<H2>Third-party services</H2>
|
||||||
<p>
|
<p>
|
||||||
Subfeed uses YouTube API Services. By using Subfeed you agree to be bound by the{" "}
|
Siftlode uses YouTube API Services. By using Siftlode you agree to be bound by the{" "}
|
||||||
<LegalLink href="https://www.youtube.com/t/terms">YouTube Terms of Service</LegalLink>.
|
<LegalLink href="https://www.youtube.com/t/terms">YouTube Terms of Service</LegalLink>.
|
||||||
Your data is handled as described in our{" "}
|
Your data is handled as described in our{" "}
|
||||||
<a href="/privacy" className="text-accent hover:underline">Privacy Policy</a> and, for
|
<a href="/privacy" className="text-accent hover:underline">Privacy Policy</a> and, for
|
||||||
|
|
@ -38,7 +38,7 @@ export default function Terms() {
|
||||||
<p>
|
<p>
|
||||||
To the maximum extent permitted by law, the service is provided without warranty, and
|
To the maximum extent permitted by law, the service is provided without warranty, and
|
||||||
the operator is not liable for any indirect, incidental, or consequential damages, or
|
the operator is not liable for any indirect, incidental, or consequential damages, or
|
||||||
for any loss of data, arising from your use of Subfeed. Your sole remedy if you are
|
for any loss of data, arising from your use of Siftlode. Your sole remedy if you are
|
||||||
dissatisfied is to stop using the service and revoke its access to your account.
|
dissatisfied is to stop using the service and revoke its access to your account.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Dumps the Subfeed Postgres database to backups\subfeed-<timestamp>.dump (custom format).
|
# Dumps the Siftlode Postgres database to backupssiftlode-<timestamp>.dump (custom format).
|
||||||
# Run from the project root: .\scripts\backup.ps1
|
# Run from the project root: .\scripts\backup.ps1
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "subfeed" }
|
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "subfeed" }
|
||||||
$dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "subfeed" }
|
$dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "subfeed" }
|
||||||
New-Item -ItemType Directory -Force -Path "backups" | Out-Null
|
New-Item -ItemType Directory -Force -Path "backups" | Out-Null
|
||||||
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
|
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||||
$out = "backups\subfeed-$ts.dump"
|
$out = "backups\siftlode-$ts.dump"
|
||||||
docker compose exec -T db pg_dump -U $user -Fc $dbname | Set-Content -NoNewline -Encoding Byte $out
|
docker compose exec -T db pg_dump -U $user -Fc $dbname | Set-Content -NoNewline -Encoding Byte $out
|
||||||
Write-Output "Wrote $out"
|
Write-Output "Wrote $out"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Dumps the Subfeed Postgres database to backups/subfeed-<timestamp>.dump (custom format).
|
# Dumps the Siftlode Postgres database to backups/siftlode-<timestamp>.dump (custom format).
|
||||||
# Run from the project root: ./scripts/backup.sh
|
# Run from the project root: ./scripts/backup.sh
|
||||||
set -e
|
set -e
|
||||||
USER_NAME="${POSTGRES_USER:-subfeed}"
|
USER_NAME="${POSTGRES_USER:-subfeed}"
|
||||||
DB_NAME="${POSTGRES_DB:-subfeed}"
|
DB_NAME="${POSTGRES_DB:-subfeed}"
|
||||||
mkdir -p backups
|
mkdir -p backups
|
||||||
TS=$(date +%Y%m%d-%H%M%S)
|
TS=$(date +%Y%m%d-%H%M%S)
|
||||||
OUT="backups/subfeed-$TS.dump"
|
OUT="backups/siftlode-$TS.dump"
|
||||||
docker compose exec -T db pg_dump -U "$USER_NAME" -Fc "$DB_NAME" > "$OUT"
|
docker compose exec -T db pg_dump -U "$USER_NAME" -Fc "$DB_NAME" > "$OUT"
|
||||||
echo "Wrote $OUT"
|
echo "Wrote $OUT"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Restores a Subfeed Postgres dump created by backup.ps1 into the running db container.
|
# Restores a Siftlode Postgres dump created by backup.ps1 into the running db container.
|
||||||
# Usage: .\scripts\restore.ps1 backups\subfeed-YYYYMMDD-HHMMSS.dump
|
# Usage: .\scripts\restore.ps1 backupssiftlode-YYYYMMDD-HHMMSS.dump
|
||||||
param([Parameter(Mandatory = $true)][string]$DumpFile)
|
param([Parameter(Mandatory = $true)][string]$DumpFile)
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "subfeed" }
|
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "subfeed" }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Restores a Subfeed Postgres dump created by backup.sh into the running db container.
|
# Restores a Siftlode Postgres dump created by backup.sh into the running db container.
|
||||||
# Usage: ./scripts/restore.sh backups/subfeed-YYYYMMDD-HHMMSS.dump
|
# Usage: ./scripts/restore.sh backups/siftlode-YYYYMMDD-HHMMSS.dump
|
||||||
set -e
|
set -e
|
||||||
FILE="$1"
|
FILE="$1"
|
||||||
if [ -z "$FILE" ]; then echo "Usage: restore.sh <dumpfile>"; exit 1; fi
|
if [ -z "$FILE" ]; then echo "Usage: restore.sh <dumpfile>"; exit 1; fi
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue