Merge: rebrand to Siftlode
This commit is contained in:
commit
842c6d800e
20 changed files with 54 additions and 54 deletions
|
|
@ -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`
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "subfeed-frontend",
|
||||
"name": "siftlode-frontend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 & 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 & 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>.
|
||||
|
|
|
|||
|
|
@ -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 & 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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue