Merge: rebrand to Siftlode

This commit is contained in:
npeter83 2026-06-14 04:40:22 +02:00
commit 842c6d800e
20 changed files with 54 additions and 54 deletions

View file

@ -1,4 +1,4 @@
# Subfeed
# Siftlode
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…),
@ -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:
```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`
```

View file

@ -8,7 +8,7 @@ _DEFAULT_SECRET_KEY = "change-me-session-key"
class Settings(BaseSettings):
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"
@ -35,7 +35,7 @@ class Settings(BaseSettings):
smtp_port: int = 587
smtp_user: 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 ---
# Optional API key for public reads (channels/videos/playlistItems). When set it is

View file

@ -56,20 +56,20 @@ def send_access_approved(email: str) -> bool:
admin = _admin_contact()
body = (
"Hi,\n\n"
"Your request to use Subfeed has been approved — welcome aboard.\n\n"
"Just open Subfeed and sign in with this Google account, and your YouTube\n"
"Your request to use Siftlode has been approved — welcome aboard.\n\n"
"Just open Siftlode and sign in with this Google account, and your YouTube\n"
"subscriptions feed will be ready.\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 "")
)
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:
body = (
f"{requester} just requested access to Subfeed.\n\n"
"Open Subfeed → Settings → Account → Access requests to approve or deny it.\n\n"
f"{requester} just requested access to Siftlode.\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"
)
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)

View file

@ -32,12 +32,12 @@ from app.scheduler import shutdown_scheduler, start_scheduler
@asynccontextmanager
async def lifespan(app: FastAPI):
log.info("Subfeed starting up")
log.info("Siftlode starting up")
start_scheduler()
try:
yield
finally:
log.info("Subfeed shutting down")
log.info("Siftlode shutting down")
shutdown_scheduler()

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Subfeed</title>
<title>Siftlode</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }

View file

@ -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]:
url = RSS_URL.format(channel_id=channel_id)
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:
return []
if resp.status_code != 200:

View file

@ -4,5 +4,5 @@ set -e
echo "Applying database migrations..."
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

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Subfeed</title>
<title>Siftlode</title>
</head>
<body>
<div id="root"></div>

View file

@ -1,5 +1,5 @@
{
"name": "subfeed-frontend",
"name": "siftlode-frontend",
"private": true,
"type": "module",
"scripts": {

View file

@ -33,7 +33,7 @@ export default function Header({
className="text-xl font-bold tracking-tight select-none"
title="Feed"
>
Sub<span className="text-accent">feed</span>
Sift<span className="text-accent">lode</span>
</button>
<SyncStatus />

View file

@ -32,7 +32,7 @@ export default function Login() {
<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="text-3xl font-bold tracking-tight">
Sub<span className="text-accent">feed</span>
Sift<span className="text-accent">lode</span>
</div>
<p className="text-muted my-5 leading-relaxed">
A self-hosted, filterable feed of your YouTube subscriptions sort, tag, and

View file

@ -18,9 +18,9 @@ function ConsentHeadsUp() {
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-amber-400" />
<p className="text-xs leading-relaxed text-fg/80">
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> {" "}
<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{" "}
<a
href="https://myaccount.google.com/permissions"
@ -90,7 +90,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
</div>
<h2 className="text-lg font-semibold">Connect your YouTube subscriptions</h2>
<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
subscribed to on YouTube. It never posts, deletes, or changes anything with this.
</p>
@ -119,7 +119,7 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
</div>
<h2 className="text-lg font-semibold">You're connected</h2>
<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
this needs an extra write permission. Skip it to stay read-only; you can always
enable it later in Settings.

View file

@ -492,7 +492,7 @@ function AccessRow({
Granted
</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
onClick={onEnable}
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
title="Read subscriptions (your feed)"
granted={me.can_read}
grantedHint="Granted. Subfeed 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."
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. Siftlode can't modify anything with it."
onEnable={() => {
window.location.href = "/auth/upgrade?access=read";
}}
@ -541,8 +541,8 @@ function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
<AccessRow
title="Unsubscribe (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."
enableHint="Lets Subfeed unsubscribe from channels on YouTube for you. Optional — skip it to stay read-only."
grantedHint="Granted. You can unsubscribe from channels on YouTube from inside Siftlode. It only writes when you ask it to."
enableHint="Lets Siftlode unsubscribe from channels on YouTube for you. Optional — skip it to stay read-only."
onEnable={() => {
window.location.href = "/auth/upgrade?access=write";
}}

View file

@ -24,7 +24,7 @@ export default function LegalLayout({
<div className="min-h-screen bg-bg text-fg">
<div className="mx-auto w-[min(92vw,720px)] py-12">
<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>
<div className="glass rounded-2xl p-8">
<h1 className="text-2xl font-semibold">{title}</h1>

View file

@ -4,14 +4,14 @@ export default function PrivacyPolicy() {
return (
<LegalLayout title="Privacy Policy">
<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
filterable feed. This policy explains what data Subfeed accesses, how it is used, and
the choices you have. By using Subfeed you agree to this policy.
filterable feed. This policy explains what data Siftlode accesses, how it is used, and
the choices you have. By using Siftlode you agree to this policy.
</p>
<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">
<li>
<span className="text-fg font-medium">Sign-in (always):</span> your basic Google
@ -31,13 +31,13 @@ export default function PrivacyPolicy() {
</li>
</ul>
<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.
</p>
<H2>How we use it</H2>
<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
unsubscribing) that you initiate. We do not use it for advertising, profiling, or any
purpose unrelated to the app.
@ -45,7 +45,7 @@ export default function PrivacyPolicy() {
<H2>Limited Use disclosure</H2>
<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{" "}
<LegalLink href="https://developers.google.com/terms/api-services-user-data-policy">
Google API Services User Data Policy
@ -65,7 +65,7 @@ export default function PrivacyPolicy() {
<H2>Retention &amp; deletion</H2>
<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{" "}
<LegalLink href="https://myaccount.google.com/permissions">
myaccount.google.com/permissions
@ -80,13 +80,13 @@ export default function PrivacyPolicy() {
<H2>Cookies</H2>
<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.
</p>
<H2>YouTube &amp; Google</H2>
<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>,
and your use of Google data is subject to the{" "}
<LegalLink href="https://policies.google.com/privacy">Google Privacy Policy</LegalLink>.

View file

@ -4,21 +4,21 @@ export default function Terms() {
return (
<LegalLayout title="Terms of Service">
<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
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>
<H2>The service</H2>
<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
be slowed, interrupted, or discontinued at any time without notice.
</p>
<H2>Your account &amp; acceptable use</H2>
<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
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.
@ -26,7 +26,7 @@ export default function Terms() {
<H2>Third-party services</H2>
<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>.
Your data is handled as described in our{" "}
<a href="/privacy" className="text-accent hover:underline">Privacy Policy</a> and, for
@ -38,7 +38,7 @@ export default function Terms() {
<p>
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
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.
</p>

View file

@ -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
$ErrorActionPreference = "Stop"
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "subfeed" }
$dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "subfeed" }
New-Item -ItemType Directory -Force -Path "backups" | Out-Null
$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
Write-Output "Wrote $out"

View file

@ -1,11 +1,11 @@
#!/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
set -e
USER_NAME="${POSTGRES_USER:-subfeed}"
DB_NAME="${POSTGRES_DB:-subfeed}"
mkdir -p backups
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"
echo "Wrote $OUT"

View file

@ -1,5 +1,5 @@
# Restores a Subfeed Postgres dump created by backup.ps1 into the running db container.
# Usage: .\scripts\restore.ps1 backups\subfeed-YYYYMMDD-HHMMSS.dump
# Restores a Siftlode Postgres dump created by backup.ps1 into the running db container.
# Usage: .\scripts\restore.ps1 backupssiftlode-YYYYMMDD-HHMMSS.dump
param([Parameter(Mandatory = $true)][string]$DumpFile)
$ErrorActionPreference = "Stop"
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "subfeed" }

View file

@ -1,6 +1,6 @@
#!/bin/sh
# Restores a Subfeed Postgres dump created by backup.sh into the running db container.
# Usage: ./scripts/restore.sh backups/subfeed-YYYYMMDD-HHMMSS.dump
# Restores a Siftlode Postgres dump created by backup.sh into the running db container.
# Usage: ./scripts/restore.sh backups/siftlode-YYYYMMDD-HHMMSS.dump
set -e
FILE="$1"
if [ -z "$FILE" ]; then echo "Usage: restore.sh <dumpfile>"; exit 1; fi