Merge: promote dev to prod

This commit is contained in:
npeter83 2026-06-26 00:37:25 +02:00
commit 1621695cec
24 changed files with 68 additions and 45 deletions

View file

@ -1 +1 @@
0.16.0
0.16.1

View file

@ -65,6 +65,10 @@ class Settings(BaseSettings):
# Optional API key for public reads (channels/videos/playlistItems). When set it is
# preferred for shared backfill/enrichment so it doesn't depend on a specific user.
youtube_api_key: str = ""
# Optional HTTP(S) proxy for outbound YouTube Data API calls. Set this to send the
# scheduler's googleapis traffic through a fixed-IP host (e.g. the VPS over WireGuard) so an
# IP-restricted API key keeps working even when this host's public IP is dynamic.
youtube_api_proxy: str = ""
# Daily quota budget (units). Default leaves headroom under the 10,000/day free limit.
quota_daily_budget: int = 9000
# Recent-first backfill: how far back to fetch on the first pass per channel.

View file

@ -50,6 +50,8 @@ SPECS: tuple[ConfigSpec, ...] = (
ConfigSpec("autotag_title_sample", "int", "batch", "autotag_title_sample", min=1, max=1_000),
# --- YouTube Data API key (secret; optional — public reads prefer it over OAuth) ---
ConfigSpec("youtube_api_key", "str", "youtube", "youtube_api_key", secret=True),
# Optional egress proxy for YouTube API calls (fixed-IP host for an IP-restricted key).
ConfigSpec("youtube_api_proxy", "str", "youtube", "youtube_api_proxy"),
# --- Google OAuth client (Sign in with Google; both encrypted, env fallback). Set from the
# install wizard / Configuration page so no restart is needed when the creds change. ---
ConfigSpec("google_client_id", "str", "google", "google_client_id", secret=True),

View file

@ -44,7 +44,10 @@ class YouTubeClient:
def __init__(self, db, user: User):
self.db = db
self.user = user
self._http = httpx.Client(timeout=30.0)
# Optionally route all YouTube traffic through a fixed-IP egress proxy (e.g. the VPS
# over WireGuard) so an IP-restricted API key keeps working from a dynamic-IP host.
proxy = sysconfig.get_str(db, "youtube_api_proxy") or None
self._http = httpx.Client(timeout=30.0, proxy=proxy)
def __enter__(self) -> "YouTubeClient":
return self

View file

@ -242,10 +242,10 @@ function AdminInvites() {
});
const add = useMutation({
mutationFn: (email: string) => api.addInvite(email),
onSuccess: () => {
onSuccess: (_d, email) => {
setNewEmail("");
refresh();
notify({ level: "success", message: t("settings.invites.addedToWhitelist") });
notify({ level: "success", message: t("settings.invites.addedToWhitelist", { email }) });
},
onError: () => notify({ level: "error", message: t("settings.invites.addFailed") }),
});
@ -324,10 +324,10 @@ function AdminDemo() {
const add = useMutation({
mutationFn: (email: string) => api.addDemoWhitelist(email),
onSuccess: () => {
onSuccess: (_d, email) => {
setNewEmail("");
qc.invalidateQueries({ queryKey: ["demo-whitelist"] });
notify({ level: "success", message: t("settings.demo.added") });
notify({ level: "success", message: t("settings.demo.added", { email }) });
},
onError: () => notify({ level: "error", message: t("settings.demo.addFailed") }),
});

View file

@ -157,20 +157,20 @@ export default function Channels({
onError: (e) => notifyActionError(e, "channels.notify.syncFailed"),
});
const unsubscribe = useMutation({
mutationFn: (id: string) => api.unsubscribeChannel(id),
onSuccess: () => {
mutationFn: (v: { id: string; name: string }) => api.unsubscribeChannel(v.id),
onSuccess: (_d, v) => {
qc.invalidateQueries({ queryKey: ["channels"] });
qc.invalidateQueries({ queryKey: ["my-status"] });
notify({ level: "success", message: t("channels.notify.unsubscribed") });
notify({ level: "success", message: t("channels.notify.unsubscribed", { name: v.name }) });
},
onError: (e) => notifyActionError(e, "channels.notify.unsubscribeFailed"),
});
const resetBackfill = useMutation({
mutationFn: (id: string) => api.resetChannelBackfill(id),
onSuccess: () => {
mutationFn: (v: { id: string; name: string }) => api.resetChannelBackfill(v.id),
onSuccess: (_d, v) => {
qc.invalidateQueries({ queryKey: ["channels"] });
qc.invalidateQueries({ queryKey: ["my-status"] });
notify({ level: "success", message: t("channels.notify.resetDone") });
notify({ level: "success", message: t("channels.notify.resetDone", { name: v.name }) });
},
onError: (e) => notifyActionError(e, "channels.notify.resetFailed"),
});
@ -210,7 +210,7 @@ export default function Channels({
confirmLabel: t("channels.row.unsubscribeOnYoutube"),
danger: true,
});
if (ok) unsubscribe.mutate(c.id);
if (ok) unsubscribe.mutate({ id: c.id, name: c.title ?? c.id });
};
const onReset = async (c: ManagedChannel) => {
const ok = await confirm({
@ -218,7 +218,7 @@ export default function Channels({
message: t("channels.confirmReset", { name: c.title ?? c.id }),
confirmLabel: t("channels.row.backfillThis"),
});
if (ok) resetBackfill.mutate(c.id);
if (ok) resetBackfill.mutate({ id: c.id, name: c.title ?? c.id });
};
const columns: Column<ManagedChannel>[] = [

View file

@ -353,11 +353,12 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
danger: true,
});
if (!ok) return;
const plName = detail.kind === "watch_later" ? t("playlists.watchLater") : detail.name;
setReverting(true);
try {
await api.revertPlaylist(selectedId);
refreshAll();
notify({ level: "success", message: t("playlists.revertDone") });
notify({ level: "success", message: t("playlists.revertDone", { name: plName }) });
} catch {
notify({ level: "warning", message: t("playlists.revertFailed") });
} finally {
@ -367,11 +368,12 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
async function pushToYoutube() {
if (selectedId == null || !detail || pushing) return;
const plName = detail.kind === "watch_later" ? t("playlists.watchLater") : detail.name;
setPushing(true);
try {
const plan = await api.playlistPushPlan(selectedId);
if (plan.action === "update" && !plan.to_insert && !plan.to_delete && !plan.to_reorder) {
notify({ level: "info", message: t("playlists.pushUpToDate") });
notify({ level: "info", message: t("playlists.pushUpToDate", { name: plName }) });
return;
}
if (!plan.affordable) {
@ -410,7 +412,7 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
if (r.failures.length) {
notify({ level: "warning", message: t("playlists.pushPartial", { count: r.failures.length }) });
} else {
notify({ level: "success", message: t("playlists.pushDone") });
notify({ level: "success", message: t("playlists.pushDone", { name: plName }) });
}
} catch {
notify({ level: "warning", message: t("playlists.pushFailed") });

View file

@ -137,10 +137,10 @@ export default function TagManager({
onSuccess: invalidate,
});
const del = useMutation({
mutationFn: (id: number) => api.deleteTag(id),
onSuccess: () => {
mutationFn: (v: { id: number; name: string }) => api.deleteTag(v.id),
onSuccess: (_d, v) => {
invalidate();
notify({ level: "success", message: t("tagManager.deleted") });
notify({ level: "success", message: t("tagManager.deleted", { name: v.name }) });
},
});
@ -179,7 +179,7 @@ export default function TagManager({
});
if (!ok) return;
}
del.mutate(tag.id);
del.mutate({ id: tag.id, name: tag.name });
}}
/>
))}

View file

@ -111,14 +111,14 @@
"notify": {
"synced": "{{count}} Abos synchronisiert",
"syncFailed": "Abo-Synchronisierung fehlgeschlagen",
"unsubscribed": "Bei YouTube abbestellt",
"unsubscribed": "{{name}} bei YouTube abbestellt",
"unsubscribeFailed": "Abbestellen fehlgeschlagen",
"fullHistoryRequested": "Vollständiger Verlauf für {{count}} Kanäle angefordert",
"fullHistoryFailed": "Vollständiger Verlauf konnte nicht angefordert werden",
"needYouTube": "Verbinde dein YouTube-Konto, um dies zu tun.",
"connect": "Verbinden",
"filteredByTag": "Feed nach Tag gefiltert: {{name}}",
"resetDone": "Kanal zurückgesetzt — wird neu geladen",
"resetDone": "{{name}} zurückgesetzt — wird neu geladen",
"resetFailed": "Kanal konnte nicht zurückgesetzt werden"
},
"confirmUnsubscribe": "Kanal „{{name}}“ bei YouTube abbestellen? Dies ändert dein echtes YouTube-Konto. Um ihn nur aus deinem Feed zu entfernen, blende ihn stattdessen aus.",

View file

@ -26,6 +26,7 @@
"enrich_batch_size": { "label": "Anreicherungs-Stapelgröße", "hint": "videos.list-IDs pro Aufruf (YouTube begrenzt auf 50)." },
"autotag_title_sample": { "label": "Auto-Tag-Titelstichprobe", "hint": "Pro Kanal abgetastete aktuelle Videotitel zur Spracherkennung." },
"youtube_api_key": { "label": "YouTube-API-Schlüssel", "hint": "Optional. Für öffentliche Lesezugriffe (Kanäle/Videos), damit gemeinsames Nachladen nicht vom OAuth eines Nutzers abhängt. Verschlüsselt gespeichert; nur schreibbar." },
"youtube_api_proxy": { "label": "YouTube-API-Egress-Proxy", "hint": "Optional. HTTP(S)-Proxy-URL für ausgehende YouTube-API-Aufrufe — über einen Host mit fester IP leiten, damit ein IP-beschränkter Schlüssel von einem Server mit dynamischer IP funktioniert. Leer = direkt." },
"google_client_id": { "label": "Google-Client-ID", "hint": "OAuth-2.0-Client-ID für die Google-Anmeldung. Verschlüsselt gespeichert; nur schreibbar. Beide Google-Felder leer lassen, um die Google-Anmeldung zu deaktivieren (E-Mail+Passwort funktioniert weiter)." },
"google_client_secret": { "label": "Google-Client-Secret", "hint": "OAuth-2.0-Client-Secret. Verschlüsselt gespeichert; nur schreibbar." },
"allow_registration": { "label": "Registrierung erlauben", "hint": "Wenn aktiviert, kann jeder eine E-Mail+Passwort-Registrierung einreichen. Für die Anmeldung sind weiterhin E-Mail-Bestätigung und Admin-Freigabe nötig." }

View file

@ -26,7 +26,7 @@
"revertTitle": "Lokale Änderungen verwerfen?",
"revertMsg": "Dies lädt die Wiedergabeliste von YouTube neu und verwirft deine nicht synchronisierten lokalen Änderungen (Reihenfolge, Hinzufügungen, Entfernungen, Umbenennung). Fortfahren?",
"revertConfirm": "Verwerfen & neu laden",
"revertDone": "Von YouTube neu geladen ✓",
"revertDone": "„{{name}}“ von YouTube neu geladen ✓",
"revertFailed": "Neuladen von YouTube fehlgeschlagen.",
"pushTitle": "Mit YouTube synchronisieren",
"pushConfirm": "Synchronisieren",
@ -34,8 +34,8 @@
"pushPlanUpdate": "Auf YouTube aktualisieren — {{insert}} hinzufügen, {{del}} entfernen, {{reorder}} neu anordnen? (~{{units}} Kontingenteinheiten; heute noch {{left}} übrig.)",
"pushDiverged": "{{count}} Video(s), die derzeit auf YouTube sind, werden entfernt.",
"pushNoQuota": "Nicht genug YouTube-Kontingent für heute ({{left}}) für diese Synchronisierung (~{{units}} benötigt). Versuche es morgen erneut.",
"pushUpToDate": "Bereits mit YouTube synchronisiert.",
"pushDone": "Mit YouTube synchronisiert ✓",
"pushUpToDate": "„{{name}}“ ist bereits mit YouTube synchronisiert.",
"pushDone": "„{{name}}“ mit YouTube synchronisiert ✓",
"pushPartial": "Synchronisiert, aber {{count}} Element(e) wurden übersprungen.",
"pushFailed": "Synchronisierung mit YouTube fehlgeschlagen.",
"deleteOnYoutubeTitle": "Auch auf YouTube löschen?",

View file

@ -93,7 +93,7 @@
"intro": "E-Mails hier können das gemeinsame Demo-Konto direkt von der Anmeldeseite aus betreten — ohne Google-Anmeldung. Sie tippen die Adresse einfach ins Feld und werden nach einem Moment eingelassen.",
"addPlaceholder": "E-Mail für die Demo auf die Whitelist…",
"add": "Hinzufügen",
"added": "Zur Demo-Whitelist hinzugefügt",
"added": "{{email}} zur Demo-Whitelist hinzugefügt",
"addFailed": "Diese E-Mail konnte nicht hinzugefügt werden",
"removeFailed": "Diese E-Mail konnte nicht entfernt werden",
"remove": "Entfernen",
@ -115,7 +115,7 @@
"approved": "{{email}} genehmigt — sie können sich jetzt anmelden",
"approveFailed": "Genehmigung fehlgeschlagen",
"denyFailed": "Ablehnung fehlgeschlagen",
"addedToWhitelist": "Zur Whitelist hinzugefügt",
"addedToWhitelist": "{{email}} zur Whitelist hinzugefügt",
"addFailed": "Diese E-Mail konnte nicht hinzugefügt werden",
"requested": "angefragt {{time}}",
"approveHint": "Genehmigen — setzt diese E-Mail auf die Whitelist und benachrichtigt sie per E-Mail, dass sie dabei sind.",

View file

@ -3,7 +3,7 @@
"channels": "{{count}} Kan.",
"onChannels": "Getaggte Kanäle",
"delete": "Löschen",
"deleted": "Tag gelöscht",
"deleted": "Tag „{{name}}“ gelöscht",
"deleteTitle": "Tag löschen",
"confirmDelete": "Tag „{{name}}“ löschen? Er wird von allen Kanälen entfernt, auf denen er liegt.",
"empty": "Noch keine Tags — füge unten einen hinzu.",

View file

@ -111,14 +111,14 @@
"notify": {
"synced": "Synced {{count}} subscriptions",
"syncFailed": "Subscription sync failed",
"unsubscribed": "Unsubscribed on YouTube",
"unsubscribed": "Unsubscribed from {{name}} on YouTube",
"unsubscribeFailed": "Unsubscribe failed",
"fullHistoryRequested": "Full history requested for {{count}} channels",
"fullHistoryFailed": "Couldn't request full history",
"needYouTube": "Connect your YouTube account to do this.",
"connect": "Connect",
"filteredByTag": "Feed filtered by tag: {{name}}",
"resetDone": "Channel reset — re-fetching now",
"resetDone": "{{name}} reset — re-fetching now",
"resetFailed": "Couldn't reset this channel"
},
"confirmUnsubscribe": "Unsubscribe from \"{{name}}\" on YouTube? This changes your real YouTube account. To just remove it from your feed, hide it instead.",

View file

@ -26,6 +26,7 @@
"enrich_batch_size": { "label": "Enrichment batch size", "hint": "videos.list ids per call (YouTube caps this at 50)." },
"autotag_title_sample": { "label": "Auto-tag title sample", "hint": "Recent video titles sampled per channel for language detection." },
"youtube_api_key": { "label": "YouTube API key", "hint": "Optional. Used for public reads (channels/videos) so shared backfill doesn't depend on a user's OAuth. Stored encrypted; write-only." },
"youtube_api_proxy": { "label": "YouTube API egress proxy", "hint": "Optional. HTTP(S) proxy URL for outbound YouTube API calls — route them through a fixed-IP host so an IP-restricted key keeps working from a dynamic-IP server. Leave empty for direct." },
"google_client_id": { "label": "Google client ID", "hint": "OAuth 2.0 client ID for Sign in with Google. Stored encrypted; write-only. Leave both Google fields empty to disable Google sign-in (email+password still works)." },
"google_client_secret": { "label": "Google client secret", "hint": "OAuth 2.0 client secret. Stored encrypted; write-only." },
"allow_registration": { "label": "Allow registration", "hint": "When on, anyone can submit an email+password registration. They still need to verify their email and be approved by an admin before they can sign in." }

View file

@ -26,7 +26,7 @@
"revertTitle": "Discard local changes?",
"revertMsg": "This reloads the playlist from YouTube and discards your unsynced local changes (order, additions, removals, rename). Continue?",
"revertConfirm": "Discard & reload",
"revertDone": "Reloaded from YouTube ✓",
"revertDone": "“{{name}}” reloaded from YouTube ✓",
"revertFailed": "Couldn't reload from YouTube.",
"pushTitle": "Sync to YouTube",
"pushConfirm": "Sync",
@ -34,8 +34,8 @@
"pushPlanUpdate": "Update on YouTube — add {{insert}}, remove {{del}}, reorder {{reorder}}? (~{{units}} quota units; {{left}} left today.)",
"pushDiverged": "{{count}} video(s) currently on YouTube will be removed.",
"pushNoQuota": "Not enough YouTube quota left today ({{left}}) for this sync (~{{units}} needed). Try again tomorrow.",
"pushUpToDate": "Already in sync with YouTube.",
"pushDone": "Synced to YouTube ✓",
"pushUpToDate": "“{{name}}” is already in sync with YouTube.",
"pushDone": "“{{name}}” synced to YouTube ✓",
"pushPartial": "Synced, but {{count}} item(s) were skipped.",
"pushFailed": "Couldn't sync to YouTube.",
"deleteOnYoutubeTitle": "Delete on YouTube too?",

View file

@ -93,7 +93,7 @@
"intro": "Emails here can enter the shared demo account straight from the login page — no Google sign-in. They just type the address into the field and are let in after a moment.",
"addPlaceholder": "Whitelist an email for demo…",
"add": "Add",
"added": "Added to the demo whitelist",
"added": "{{email}} added to the demo whitelist",
"addFailed": "Couldn't add that email",
"removeFailed": "Couldn't remove that email",
"remove": "Remove",
@ -115,7 +115,7 @@
"approved": "Approved {{email}} — they can sign in now",
"approveFailed": "Approve failed",
"denyFailed": "Deny failed",
"addedToWhitelist": "Added to the whitelist",
"addedToWhitelist": "{{email}} added to the whitelist",
"addFailed": "Couldn't add that email",
"requested": "requested {{time}}",
"approveHint": "Approve — whitelist this email and email them they're in.",

View file

@ -3,7 +3,7 @@
"channels": "{{count}} ch.",
"onChannels": "Tagged channels",
"delete": "Delete",
"deleted": "Tag deleted",
"deleted": "Tag “{{name}}” deleted",
"deleteTitle": "Delete tag",
"confirmDelete": "Delete the tag “{{name}}”? It will be removed from every channel it's on.",
"empty": "No tags yet — add one below.",

View file

@ -111,14 +111,14 @@
"notify": {
"synced": "{{count}} feliratkozás szinkronizálva",
"syncFailed": "A feliratkozások szinkronizálása sikertelen",
"unsubscribed": "Leiratkozva a YouTube-on",
"unsubscribed": "Leiratkozva a(z) {{name}} csatornáról a YouTube-on",
"unsubscribeFailed": "A leiratkozás sikertelen",
"fullHistoryRequested": "Teljes előzmény kérve {{count}} csatornához",
"fullHistoryFailed": "Nem sikerült teljes előzményt kérni",
"needYouTube": "Ehhez csatlakoztasd a YouTube-fiókod.",
"connect": "Csatlakoztatás",
"filteredByTag": "Hírfolyam szűrve címkére: {{name}}",
"resetDone": "Csatorna resetelve — újraletöltés folyamatban",
"resetDone": "{{name}} resetelve — újraletöltés folyamatban",
"resetFailed": "Nem sikerült resetelni a csatornát"
},
"confirmUnsubscribe": "Leiratkozol a(z) „{{name}}” csatornáról a YouTube-on? Ez megváltoztatja a valódi YouTube-fiókodat. Ha csak a hírfolyamodból szeretnéd eltávolítani, inkább rejtsd el.",

View file

@ -26,6 +26,7 @@
"enrich_batch_size": { "label": "Gazdagítási kötegméret", "hint": "videos.list azonosítók hívásonként (a YouTube 50-ben maximálja)." },
"autotag_title_sample": { "label": "Auto-címke címmintavétel", "hint": "Csatornánként ennyi friss videócímet mintázunk a nyelvfelismeréshez." },
"youtube_api_key": { "label": "YouTube API-kulcs", "hint": "Opcionális. Publikus olvasásokhoz (csatornák/videók), hogy a megosztott letöltés ne függjön egy felhasználó OAuth-jától. Titkosítva tárolva; csak írható." },
"youtube_api_proxy": { "label": "YouTube API egress proxy", "hint": "Opcionális. HTTP(S) proxy URL a kimenő YouTube API-hívásokhoz — fix IP-jű hoston átengedve egy IP-korlátozott kulcs dinamikus IP-jű szerverről is működik. Üresen = közvetlen." },
"google_client_id": { "label": "Google kliens-azonosító", "hint": "OAuth 2.0 kliens-azonosító a Google-bejelentkezéshez. Titkosítva tárolva; csak írható. Hagyd üresen mindkét Google-mezőt a Google-bejelentkezés kikapcsolásához (az e-mail+jelszó továbbra is működik)." },
"google_client_secret": { "label": "Google kliens-titok", "hint": "OAuth 2.0 kliens-titok. Titkosítva tárolva; csak írható." },
"allow_registration": { "label": "Regisztráció engedélyezése", "hint": "Bekapcsolva bárki beküldhet email+jelszó regisztrációt. Belépéshez továbbra is email-igazolás és admin-jóváhagyás szükséges." }

View file

@ -26,7 +26,7 @@
"revertTitle": "Eldobod a helyi módosításokat?",
"revertMsg": "Ez újratölti a listát a YouTube-ról, és eldobja a nem szinkronizált helyi módosításaidat (sorrend, hozzáadás, eltávolítás, átnevezés). Folytatod?",
"revertConfirm": "Eldobás és újratöltés",
"revertDone": "Újratöltve a YouTube-ról ✓",
"revertDone": "„{{name}}” újratöltve a YouTube-ról ✓",
"revertFailed": "Nem sikerült újratölteni a YouTube-ról.",
"pushTitle": "Szinkron YouTube-ra",
"pushConfirm": "Szinkron",
@ -34,8 +34,8 @@
"pushPlanUpdate": "Frissítés a YouTube-on — {{insert}} hozzáadása, {{del}} eltávolítása, {{reorder}} átrendezése? (~{{units}} kvótaegység; ma még {{left}} maradt.)",
"pushDiverged": "{{count}} videó, amely jelenleg a YouTube-on van, el lesz távolítva.",
"pushNoQuota": "Nincs elég YouTube-kvóta mára ({{left}}) ehhez a szinkronhoz (~{{units}} kell). Próbáld újra holnap.",
"pushUpToDate": "Már szinkronban van a YouTube-bal.",
"pushDone": "Szinkronizálva a YouTube-ra ✓",
"pushUpToDate": "A(z) „{{name}}” már szinkronban van a YouTube-bal.",
"pushDone": "„{{name}}” szinkronizálva a YouTube-ra ✓",
"pushPartial": "Szinkronizálva, de {{count}} elem kimaradt.",
"pushFailed": "Nem sikerült a YouTube-ra szinkronizálás.",
"deleteOnYoutubeTitle": "Törlöd a YouTube-on is?",

View file

@ -93,7 +93,7 @@
"intro": "Az itt szereplő e-mailek a bejelentkező oldalról közvetlenül beléphetnek a közös demo fiókba — Google-bejelentkezés nélkül. Csak beírják a címet a mezőbe, és pár pillanat múlva bekerülnek.",
"addPlaceholder": "E-mail fehérlistára a demóhoz…",
"add": "Hozzáadás",
"added": "Hozzáadva a demo fehérlistához",
"added": "{{email}} hozzáadva a demo fehérlistához",
"addFailed": "Nem sikerült hozzáadni ezt az e-mailt",
"removeFailed": "Nem sikerült eltávolítani ezt az e-mailt",
"remove": "Eltávolítás",
@ -115,7 +115,7 @@
"approved": "Jóváhagyva: {{email}} — most már be tud jelentkezni",
"approveFailed": "A jóváhagyás sikertelen",
"denyFailed": "Az elutasítás sikertelen",
"addedToWhitelist": "Hozzáadva a fehérlistához",
"addedToWhitelist": "{{email}} hozzáadva a fehérlistához",
"addFailed": "Nem sikerült hozzáadni ezt az e-mailt",
"requested": "kérve: {{time}}",
"approveHint": "Jóváhagyás — fehérlistára teszi ezt az e-mailt és e-mailben értesíti őket, hogy bekerültek.",

View file

@ -3,7 +3,7 @@
"channels": "{{count}} csat.",
"onChannels": "Címkézett csatornák",
"delete": "Törlés",
"deleted": "Címke törölve",
"deleted": "„{{name}}” címke törölve",
"deleteTitle": "Címke törlése",
"confirmDelete": "Törlöd a(z) „{{name}}” címkét? Minden csatornáról lekerül, amin rajta van.",
"empty": "Még nincs címke — adj hozzá lentebb.",

View file

@ -14,6 +14,15 @@ export interface ReleaseEntry {
}
export const RELEASE_NOTES: ReleaseEntry[] = [
{
version: "0.16.1",
date: "2026-06-26",
summary: "Clearer action messages, and a fix for background syncing on self-hosted setups.",
fixes: [
"Action confirmations now name what they acted on — which channel was unsubscribed or reset, which address was added to a list, which tag was deleted, and which playlist was reloaded or synced.",
"Background syncing keeps working when the server's public IP changes: an instance can now be pointed at a fixed-IP egress proxy, so an IP-restricted YouTube API key no longer breaks after a network change.",
],
},
{
version: "0.16.0",
date: "2026-06-26",