diff --git a/.gitignore b/.gitignore index 22979bd..8319981 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,3 @@ tiles/build/ *.mbtiles tiles/build.log tiles/.DS_Store - -# Album-Build: Zwischendateien (ZIPs + Thumbnail werden committet) -tools/album-build/dist/ diff --git a/MARKETING.md b/MARKETING.md index c52125d..4692ffe 100644 --- a/MARKETING.md +++ b/MARKETING.md @@ -2,7 +2,7 @@ **Single Source of Truth fürs Marketing.** Vor jeder Aktion hier prüfen, danach updaten — so wird nichts doppelt gemacht, vergessen oder übersehen. Pflege: René + Claude. -_Stand: 2026-06-15_ +_Stand: 2026-06-09_ > Diese Datei = Planung & Checkliste. Für **Live-Daten** (User-Meilenstein, Kanal-Tracking) lohnt zusätzlich ein Marketing-Tab im **Admin-Bereich** — siehe „Ausbau" unten. @@ -16,7 +16,6 @@ _Stand: 2026-06-15_ | Empfehlung / Referral | 🟡 Infra da (`referral_code`) | Empfehlungs-QR + Tracking sichtbar machen | | Partner-Programm | 🟢 Infra komplett (v1265, 07.06.) | Partner einladen! Showcase `#partner`, Pro gratis, Partner-Dashboard, QR-Kontingente (Druck-PDF) mit Einzel-Code-Tracking, Dank-Mails mit Statistik, Pause-Notbremse für geleakte Codes. Onboarding: Admin → Code anlegen → Partner-Badge → Besitzer zuordnen | | Influencer | 🟡 2 Runden (Mai), kaum Resonanz | Runde 3 erst ab ~50 aktiven Usern — jetzt mit Partner-Paket als konkretem Angebot | -| Social / Short-Video (Reels/Shorts/TikTok) | 💡 Plan steht (15.06.) | Pilot-Clip „Platsch!" (Sommer-Timing) produzieren — Details unten unter „Song-Reel" | | Presse / Blogs | 🟡 1 Runde, kaum Resonanz | keine Massenwelle; Nische zuerst | | Verzeichnisse / Listings | ⬜ offen | Product Hunt, PWA-Dirs, Google Business EBE | | SEO / KI-Auffindbarkeit | 🟡 technisch optimiert | Rechtsseiten crawlbar (v1278) + 3 URLs (datenschutz/agb/impressum) am 09.06. in GSC zur Indexierung eingereicht — in ~Tagen auf „indexiert" prüfen; llms.txt aktuell. Nächster echter Hebel: Backlinks (Blog-Testberichte) | @@ -38,10 +37,10 @@ Legende: 🟢 läuft/erledigt · 🟡 angefangen · ⬜ offen · 💡 Idee · - [ ] **Verzeichnisse** — Product Hunt, progressivewebappstore.com, pwafire.org/directory, Google Business (Ebersberg). - [ ] **Landing-Page-Redesign** nach Briefing (3 Zielgruppen-Einstiege Hundebesitzer/Züchter/Welpenkäufer, Outcomes statt Features, Züchter-SaaS prominent, Datenschutz als Argument, Gründer-Story + Foto). - [ ] **Messung einbauen** — „Wie hast du von uns gehört?" im Onboarding + QR-refs pro Kanal. -- [ ] **Song-Reel / Short-Video-Serie** — Plan ausgearbeitet, siehe „Details je Kanal → Song-Reel / Short-Video" unten. **Nächster Schritt:** Pilot-Clip „Platsch!" (Sommer-Timing jetzt ideal). +- [ ] **Song-Reel** — die Album-Songs + App-Screenshots/Hundefotos → **Reel/YouTube-Video** für Social. Album ist erweiterbar (neuer Suno-Pro-Song = MP3 + Array-Zeile). ## ✅ Erledigt -- [x] **Ban-Yaro-Album** — **7 eigene Songs** (Ban Yaro Blues, Ban Yaro Mobil, Amy, Beim Friseur, Leckerli-Paradies, Platsch!, Bester Freund) als Album-Modal in der WELT-Welt — Prod v1300. Alle **Suno Pro** = kommerziell lizenziert (granted commercial rights, Suno nimmt 0 % Royalties). (Lektion: Lizenz hängt am Generierungszeitpunkt; neu generieren ≠ neu downloaden, per MD5 geprüft.) +- [x] **Ban-Yaro-Album** — 3 eigene Songs (Ban Yaro Blues, Ban Yaro Mobil, Amy) als Album-Modal in der WELT-Welt — 14.06., Prod v1297. Alle **Suno Pro** = kommerziell lizenziert. (Lektion: Lizenz hängt am Generierungszeitpunkt; neu generieren ≠ neu downloaden, per MD5 geprüft.) - [x] 1000 Flyer A5 (zweiseitig) gedruckt — 03.06.2026 - [x] iOS-App nativ gebaut + **im App Store freigegeben** (Ban Yaro Go, 09.06.) — Details im Repo `banyaro-ios` - [x] Landing-Promotion für „Ban Yaro Go" LIVE (iOS-Abschnitt + Profil, eigenes braunes App-Store-Badge; Hero bewusst ohne Badge) — 09.06., Prod v1278 @@ -72,42 +71,6 @@ Tag recherchiert: **HID Laundry Tag 16 mm** (shopnfc, SKU RE-ICO2-16, ~1 €/Stk ### Flyer Print: A5 zweiseitig, Quelle `promotion/flyer_a5_allgemein.html` + `flyer_a5_rueckseite.html`, QR → banyaro.app. Vorderseite = alle Hundebesitzer, Rückseite stark Züchter-fokussiert. -### Song-Reel / Short-Video - -**Ziel:** Das Album ist kein Einnahme-Produkt (Streaming zahlt ~3–4 €/1000 Streams), sondern ein **Top-of-Funnel-Marketing-Motor**. Ein ohrwurmiger Hundesong ist von Natur aus teilbar → Awareness → App-Installs. **Erfolgskennzahl = Installs pro Clip, nicht Tantiemen.** - -**Lizenz/Compliance:** Social-Reels (IG/TikTok/Shorts) brauchen **keine** DDEX-KI-Flag — die gilt nur bei Streaming-Distribution (Spotify/Apple). Optional sympathischer Hinweis „selbst gemacht 🎸 (mit KI)" in der Caption. Suno-Pro-Songs sind kommerziell freigegeben. - -**Song → Clip-Konzept** (visuelle/lustige Songs zuerst short-form, Balladen long-form): - -| Song | Sub | Clip-Idee | Format | -|---|---|---|---| -| **Platsch!** | Ab ins kühle Nass | Hund springt ins Wasser, Zeitlupe beim Sprung → Drop auf den Beat. **Sommer-Timing = JETZT.** | Short (Pilot) | -| **Leckerli-Paradies** | Voller Napf, volles Glück | Hund vor vollem Napf / im Tierladen, große Augen, Tierladen-Boogie. Sehr relatable. | Short | -| **Beim Friseur** | Halbes Fell, Energie pur | Grooming Vorher/Nachher, lustiger Schnitt. | Short | -| **Ban Yaro Mobil** | Erste Fahrt im Anhänger | Hund im Anhänger/Auto, Ohren im Fahrtwind. | Short | -| **Bester Freund** | Du und ich | Emotionale Foto-Montage Mensch-Hund-Bindung. | YouTube-Lyric + Story-Reel | -| **Amy** | Eine Liebesromanze | Zwei-Hunde-Story. | YouTube-Lyric | -| **Ban Yaro Blues** | Die Hymne | Marken-Hymne = Brand-/Pinned-Video. | YouTube + Pinned | - -**Clip-Bauplan (Template, 15–30 s, 9:16):** -1. **0–1,5 s Hook:** stärkster visueller Moment + stärkste Textzeile als Caption-Pop. -2. **1,5–~20 s:** 3–5 schnelle Hunde-Clips, **auf den Beat** geschnitten. -3. **letzte ~3 s CTA:** App-Logo + „Gratis-App für Hundemenschen → banyaro.app" + Pfoten-Badge. -4. **durchgehend:** animierte **Lyric-Untertitel** (Sound-off-Tauglichkeit — die meisten scrollen stumm!). - -**Footage:** Bans & Yaros eigenes Material (Renés Hunde) + App-Screenshots wo passend. Schnitt z. B. in **CapCut** (kostenlos, Beat-Marker + Auto-Captions). - -**Distribution & Tracking:** -- Kanäle: **Instagram Reels · TikTok · YouTube Shorts** (gleicher Clip) + Facebook (lokale EBE-Gruppen). -- **Per-Kanal-ref-Link** (on-screen + in Bio/Pinned): `banyaro.app/?ref=reel-ig` · `?ref=reel-tt` · `?ref=reel-yt` → zählbar (siehe „Messung"). -- Hashtags: #hundeliebe #dogsofinstagram #hundsetiktok + lokal #ebersberg. -- Balladen (Bester Freund, Amy, Blues) zusätzlich als **volles Lyric-Video auf YouTube** = evergreen + SEO. - -**Cadence:** Lead-Clips (Platsch!, Leckerli, Friseur, Mobil) in **einer Batch-Session** produzieren, dann **2/Woche** posten über ~2–3 Wochen. Erst der Pilot, dann Batch. - -**Erster Schritt:** **1 Pilot-Clip „Platsch!"** (Saison) als Test posten → Reaktion messen, bevor die Batch läuft. - ## 🚀 Ausbau: Live-Tool im Admin-Bereich (optional) Diese Datei deckt Planung/Checkliste ab (Claude pflegt sie). Der **Admin-Bereich** lohnt sich für die Teile mit echten Daten: - **User-Meilenstein-Anzeige** (aktive User) → blendet automatisch den „Outreach Runde 3"-Hinweis ein, sobald ~50 erreicht. diff --git a/Makefile b/Makefile index 1906455..7fc461a 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ TAR_EXCLUDE := --exclude='.git' \ --exclude='./.DS_Store' .PHONY: help deploy deploy-clean staging release sync push restart build stop status \ - logs logs-f shell db dev clean-cache check-ssh reports bump test tiles tiles-deploy album + logs logs-f shell db dev clean-cache check-ssh reports bump test tiles tiles-deploy # ---------------------------------------------------------- # SSH-Prüfung — Abhängigkeit aller DS-Befehle @@ -343,13 +343,6 @@ bump: sed -i.bak -E "s/\?v=[0-9]+/?v=$$NEW/g" backend/static/landing.html && rm -f backend/static/landing.html.bak; \ echo " ✓ APP_VER $$CUR → $$NEW (VERSION, sw.js, app.js, index.html, landing.html aktualisiert)" -# ---------------------------------------------------------- -# ALBUM — die zwei Download-ZIPs (DE+EN) neu bauen (Cover, ID3-Tags, Liner Notes) -# Nur nötig, wenn sich Songs/Cover/Beschreibung ändern. Braucht ImageMagick + ffmpeg. -# ---------------------------------------------------------- -album: - @bash tools/album-build/build.sh - # ---------------------------------------------------------- # TEST — Smoke-Tests gegen isolierte Test-DB (kein Docker, kein DS) # ---------------------------------------------------------- diff --git a/VERSION b/VERSION index 61a4199..16b7561 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1303 \ No newline at end of file +1300 \ No newline at end of file diff --git a/backend/database.py b/backend/database.py index 8fad182..a26bd07 100644 --- a/backend/database.py +++ b/backend/database.py @@ -522,7 +522,6 @@ def _migrate(conn_factory): # Forum Sprint 11: erweiterte Thread-Felder ("forum_threads", "foto_urls", "TEXT"), ("forum_threads", "is_pinned", "INTEGER NOT NULL DEFAULT 0"), - ("forum_threads", "pin_scope", "TEXT NOT NULL DEFAULT 'global'"), ("forum_threads", "is_locked", "INTEGER NOT NULL DEFAULT 0"), ("forum_threads", "is_deleted", "INTEGER NOT NULL DEFAULT 0"), ("forum_threads", "likes", "INTEGER NOT NULL DEFAULT 0"), diff --git a/backend/main.py b/backend/main.py index 13da5cf..21a5069 100644 --- a/backend/main.py +++ b/backend/main.py @@ -375,8 +375,6 @@ app.mount("/js", StaticFiles(directory=f"{STATIC_DIR}/js"), name="js") app.mount("/icons", StaticFiles(directory=f"{STATIC_DIR}/icons"), name="icons") app.mount("/img", StaticFiles(directory=f"{STATIC_DIR}/img"), name="img") app.mount("/sounds", StaticFiles(directory=f"{STATIC_DIR}/sounds"), name="sounds") # Yaro-Navi-Sounds -os.makedirs(f"{STATIC_DIR}/downloads", exist_ok=True) # Album-ZIPs (vom Build-Skript erzeugt) -app.mount("/downloads", StaticFiles(directory=f"{STATIC_DIR}/downloads"), name="downloads") # Selbst-gehostete Vektor-Tiles (.pmtiles) — liegen im data-Volume, NICHT im Image. # WICHTIG: Starlettes StaticFiles/FileResponse liefert hinter unserer BaseHTTPMiddleware diff --git a/backend/routes/forum.py b/backend/routes/forum.py index f8943ac..58d03b4 100644 --- a/backend/routes/forum.py +++ b/backend/routes/forum.py @@ -42,7 +42,6 @@ class PostCreate(BaseModel): class ThreadPatch(BaseModel): is_pinned: Optional[int] = None is_locked: Optional[int] = None - pin_scope: Optional[str] = None # 'global' (überall oben) | 'kategorie' (nur im Thema oben) class ThreadUpdate(BaseModel): titel: Optional[str] = Field(None, max_length=200) @@ -72,15 +71,6 @@ class ResolveReport(BaseModel): resolved: int = 1 -def _can_moderate(user) -> bool: - """Admin ODER Moderator dürfen moderieren (pin/lock/löschen). - Wichtig: Admins haben nicht zwingend das is_moderator-Flag gesetzt — - daher zusätzlich die Rolle prüfen (analog auth.require_moderator).""" - if not user: - return False - return user.get('rolle') in ('admin', 'moderator') or bool(user.get('is_moderator')) - - # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ @@ -136,13 +126,12 @@ async def list_threads( user=Depends(get_current_user_optional), ): uid = user['id'] if user else None - has_cat = bool(kategorie and kategorie != 'alle') with db() as conn: q = """ SELECT t.id, t.kategorie, t.titel, SUBSTR(t.text, 1, 120) AS text_preview, t.antworten, t.likes, t.views, - t.is_pinned, t.pin_scope, t.is_locked, t.foto_urls, + t.is_pinned, t.is_locked, t.foto_urls, t.created_at, t.user_id, u.name AS autor_name, u.founder_number AS autor_founder_number FROM forum_threads t @@ -150,18 +139,13 @@ async def list_threads( WHERE t.is_deleted = 0 """ params = [] - if has_cat: + if kategorie and kategorie != 'alle': q += " AND t.kategorie = ?" params.append(kategorie) if search: q += " AND (t.titel LIKE ? OR t.text LIKE ?)" params.extend([f'%{search}%', f'%{search}%']) - # Kategorie-Ansicht: globale UND Themen-Pins steigen nach oben. - # "Alle"-Ansicht: nur globale Pins oben — Themen-Pins bleiben in ihrem Thema. - if has_cat: - q += " ORDER BY t.is_pinned DESC, t.created_at DESC LIMIT ? OFFSET ?" - else: - q += " ORDER BY (t.is_pinned = 1 AND t.pin_scope = 'global') DESC, t.created_at DESC LIMIT ? OFFSET ?" + q += " ORDER BY t.is_pinned DESC, t.created_at DESC LIMIT ? OFFSET ?" params.extend([limit, offset]) rows = conn.execute(q, params).fetchall() @@ -339,7 +323,7 @@ async def delete_thread(thread_id: int, user=Depends(get_current_user)): ).fetchone() if not thread: raise HTTPException(404, "Thread nicht gefunden.") - if thread['user_id'] != user['id'] and not _can_moderate(user): + if thread['user_id'] != user['id'] and not user.get('is_moderator'): raise HTTPException(403, "Keine Berechtigung.") conn.execute( "UPDATE forum_threads SET is_deleted = 1 WHERE id = ?", (thread_id,) @@ -351,7 +335,7 @@ async def delete_thread(thread_id: int, user=Depends(get_current_user)): # ------------------------------------------------------------------ @router.patch("/threads/{thread_id}") async def patch_thread(thread_id: int, data: ThreadPatch, user=Depends(get_current_user)): - if not _can_moderate(user): + if not user.get('is_moderator'): raise HTTPException(403, "Nur Moderatoren können Threads bearbeiten.") with db() as conn: thread = conn.execute( @@ -361,8 +345,6 @@ async def patch_thread(thread_id: int, data: ThreadPatch, user=Depends(get_curre raise HTTPException(404, "Thread nicht gefunden.") updates = data.model_dump(exclude_none=True) - if 'pin_scope' in updates and updates['pin_scope'] not in ('global', 'kategorie'): - raise HTTPException(400, "Ungültiger pin_scope (erlaubt: 'global', 'kategorie').") if updates: cols = ', '.join(f"{k} = ?" for k in updates) conn.execute( @@ -494,7 +476,7 @@ async def delete_post(post_id: int, user=Depends(get_current_user)): ).fetchone() if not post: raise HTTPException(404, "Beitrag nicht gefunden.") - if post['user_id'] != user['id'] and not _can_moderate(user): + if post['user_id'] != user['id'] and not user.get('is_moderator'): raise HTTPException(403, "Keine Berechtigung.") conn.execute( "UPDATE forum_posts SET is_deleted = 1 WHERE id = ?", (post_id,) @@ -522,7 +504,7 @@ async def upload_thread_foto( ).fetchone() if not thread: raise HTTPException(404, "Thread nicht gefunden.") - if thread['user_id'] != user['id'] and not _can_moderate(user): + if thread['user_id'] != user['id'] and not user.get('is_moderator'): raise HTTPException(403, "Keine Berechtigung.") existing = _parse_foto_urls(thread['foto_urls']) @@ -555,7 +537,7 @@ async def upload_post_foto( ).fetchone() if not post: raise HTTPException(404, "Beitrag nicht gefunden.") - if post['user_id'] != user['id'] and not _can_moderate(user): + if post['user_id'] != user['id'] and not user.get('is_moderator'): raise HTTPException(403, "Keine Berechtigung.") existing = _parse_foto_urls(post['foto_urls']) @@ -660,7 +642,7 @@ async def report_content(data: ReportBody, user=Depends(get_current_user)): # ------------------------------------------------------------------ @router.get("/reports") async def list_reports(user=Depends(get_current_user)): - if not _can_moderate(user): + if not user.get('is_moderator'): raise HTTPException(403, "Nur Moderatoren.") with db() as conn: rows = conn.execute( @@ -678,7 +660,7 @@ async def list_reports(user=Depends(get_current_user)): # ------------------------------------------------------------------ @router.patch("/reports/{report_id}") async def resolve_report(report_id: int, data: ResolveReport, user=Depends(get_current_user)): - if not _can_moderate(user): + if not user.get('is_moderator'): raise HTTPException(403, "Nur Moderatoren.") with db() as conn: conn.execute( diff --git a/backend/static/css/components.css b/backend/static/css/components.css index f3e34e7..91dfd2d 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -8307,10 +8307,6 @@ svg.empty-state-icon { .album-title { font-size: var(--text-base); font-weight: 700; color: var(--c-text); } .album-subtitle { font-size: var(--text-xs); color: var(--c-text-muted); margin-top: 2px; } .album-close { background: none; border: none; font-size: 1.5rem; line-height: 1; color: var(--c-text-muted); cursor: pointer; padding: 0 4px; } -.album-head-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; } -.album-lang { display: inline-flex; border: 1px solid var(--c-border); border-radius: var(--radius-full); overflow: hidden; background: var(--c-surface-2); } -.album-lang-btn { background: none; border: none; cursor: pointer; padding: 4px 10px; font-size: var(--text-xs); font-weight: 700; color: var(--c-text-muted); line-height: 1.4; transition: background .15s, color .15s; } -.album-lang-btn.is-active { background: var(--c-primary); color: #fff; } .album-list { display: flex; flex-direction: column; gap: var(--space-2); } .album-song { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3); border-radius: var(--radius-md); background: var(--c-surface-2); cursor: pointer; transition: background .15s; } .album-song:active { background: var(--c-border); } diff --git a/backend/static/downloads/ban-yaro-album-de.zip b/backend/static/downloads/ban-yaro-album-de.zip deleted file mode 100644 index 6cc9e85..0000000 Binary files a/backend/static/downloads/ban-yaro-album-de.zip and /dev/null differ diff --git a/backend/static/downloads/ban-yaro-album-en.zip b/backend/static/downloads/ban-yaro-album-en.zip deleted file mode 100644 index d7e52e4..0000000 Binary files a/backend/static/downloads/ban-yaro-album-en.zip and /dev/null differ diff --git a/backend/static/img/banyaro/album-thumb.jpg b/backend/static/img/banyaro/album-thumb.jpg deleted file mode 100644 index bbf38d8..0000000 Binary files a/backend/static/img/banyaro/album-thumb.jpg and /dev/null differ diff --git a/backend/static/index.html b/backend/static/index.html index 5a9d95f..eaec6fc 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -624,12 +624,12 @@ - - - - - - + + + + + + @@ -639,7 +639,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 4cbbfb9..1a76c84 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '1303'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1300'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; diff --git a/backend/static/js/pages/forum.js b/backend/static/js/pages/forum.js index c9664b6..003160d 100644 --- a/backend/static/js/pages/forum.js +++ b/backend/static/js/pages/forum.js @@ -82,8 +82,7 @@ function _fmtDate(iso) { // RENDER — Grundstruktur // ---------------------------------------------------------- function _render() { - const _u = _appState.user; - const isMod = !!(_u && (_u.rolle === 'admin' || _u.rolle === 'moderator' || _u.is_moderator)); + const isMod = !!_appState.user?.is_moderator; _container.innerHTML = `
@@ -439,7 +438,7 @@ function _fmtDate(iso) { const preview = t.text_preview ? UI.escape(t.text_preview.slice(0, 120)) + (t.text_preview.length >= 120 ? '…' : '') : ''; - const pinBadge = t.is_pinned ? `${UI.icon('push-pin')}` : ''; + const pinBadge = t.is_pinned ? `${UI.icon('push-pin')}` : ''; const lockBadge = t.is_locked ? `${UI.icon('lock')}` : ''; const fotoHtml = t.foto_preview ? /\.(mp4|mov|webm|m4v|avi)$/i.test(t.foto_preview) @@ -516,25 +515,14 @@ function _fmtDate(iso) { } const uid = _appState.user?.id; - const _u = _appState.user; - const isMod = !!(_u && (_u.rolle === 'admin' || _u.rolle === 'moderator' || _u.is_moderator)); + const isMod = !!_appState.user?.is_moderator; const isOwn = uid && uid === thread.user_id; - const pinControls = thread.is_pinned - ? ` - ${UI.icon('push-pin')} Angepinnt${thread.pin_scope === 'kategorie' ? ` (Thema „${UI.escape(thread.kategorie)}")` : ' (global)'} - - ` - : ` - `; - const modToolbar = (isMod) ? `
- ${pinControls} + @@ -689,20 +677,14 @@ function _fmtDate(iso) { }); // Moderator: pin/lock/delete - const _applyPin = async (payload) => { + document.querySelector('.forum-mod-pin')?.addEventListener('click', async () => { try { - await API.forum.patchThread(thread.id, payload); + await API.forum.patchThread(thread.id, { is_pinned: thread.is_pinned ? 0 : 1 }); UI.toast.success('Gespeichert.'); UI.modal.close(); _loadThreads(true); } catch (err) { UI.toast.error(err.message); } - }; - document.querySelector('.forum-mod-pin-global')?.addEventListener('click', - () => _applyPin({ is_pinned: 1, pin_scope: 'global' })); - document.querySelector('.forum-mod-pin-cat')?.addEventListener('click', - () => _applyPin({ is_pinned: 1, pin_scope: 'kategorie' })); - document.querySelector('.forum-mod-unpin')?.addEventListener('click', - () => _applyPin({ is_pinned: 0 })); + }); document.querySelector('.forum-mod-lock')?.addEventListener('click', async () => { try { diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index d31203b..4e21eae 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -877,35 +877,6 @@ window.Page_settings = (() => {
- -
-
Ban Yaro — das Album
-
- Ban Yaro — Album-Cover -
-
7 Songs zum Behalten 🎸
-
- Das ganze Album als Download — auf Deutsch oder Englisch. - Behalten & teilen ausdrücklich erwünscht. -
-
-
-
- - ${UI.icon('download-simple')} Deutsch · 33 MB - - - ${UI.icon('download-simple')} English · 36 MB - -
-
-
diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index 17946de..c3bf861 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -1941,11 +1941,7 @@ window.Worlds = (() => { // zentral in index.html → übersteht Re-Renders & Welt-Wechsel. const _anthem = (() => { const KEY = 'by_anthem_heard'; - const LANG_KEY = 'by_album_lang'; - // EN-Album erst sichtbar, wenn die 7 *-en.mp3 in static/sounds/ liegen. - // Aktivieren: Dateien ablegen → EN_READY = true → make bump → deploy. - const EN_READY = true; // 2026-06-16: 7 *-en.mp3 liegen in static/sounds/ - const SONGS_DE = [ + const SONGS = [ { title: 'Ban Yaro Blues', sub: 'Die Hymne', file: '/sounds/ban-yaro-blues.mp3?v=2' }, { title: 'Ban Yaro Mobil', sub: 'Erste Fahrt im Anhänger', file: '/sounds/ban-yaro-mobil.mp3' }, { title: 'Amy', sub: 'Eine Liebesromanze', file: '/sounds/amy.mp3' }, @@ -1954,19 +1950,6 @@ window.Worlds = (() => { { title: 'Platsch!', sub: 'Ab ins kühle Nass', file: '/sounds/platsch.mp3' }, { title: 'Bester Freund', sub: 'Du und ich', file: '/sounds/bester-freund.mp3' }, ]; - const SONGS_EN = [ - { title: 'Ban Yaro Blues', sub: 'The anthem', file: '/sounds/ban-yaro-blues-en.mp3' }, - { title: 'Ban Yaro Mobile', sub: 'First ride in the trailer', file: '/sounds/ban-yaro-mobil-en.mp3' }, - { title: 'Amy', sub: 'A love duet', file: '/sounds/amy-en.mp3' }, - { title: "At the Groomer's", sub: 'Half the fur, all the energy', file: '/sounds/at-the-groomers-en.mp3' }, - { title: 'Treat Paradise', sub: 'Full bowl, full heart', file: '/sounds/treat-paradise-en.mp3' }, - { title: 'Splash!', sub: 'Into the cool water', file: '/sounds/splash-en.mp3' }, - { title: 'Best Friend', sub: 'You and me', file: '/sounds/best-friend-en.mp3' }, - ]; - let _lang = (() => { - try { return (EN_READY && localStorage.getItem(LANG_KEY) === 'en') ? 'en' : 'de'; } catch (_) { return 'de'; } - })(); - const _songs = () => (_lang === 'en' && EN_READY) ? SONGS_EN : SONGS_DE; let _bound = false, _curIdx = -1; const _audio = () => document.getElementById('anthem-audio'); // Entdeckt? Server-Flag (geräteübergreifend, deploy-fest) ODER lokal (sofort/offline). @@ -2005,18 +1988,17 @@ window.Worlds = (() => { a.addEventListener('play', _sync); a.addEventListener('pause', _sync); a.addEventListener('ended', () => { // automatisch zum nächsten Song - if (_curIdx >= 0 && _curIdx < _songs().length - 1) _play(_curIdx + 1); + if (_curIdx >= 0 && _curIdx < SONGS.length - 1) _play(_curIdx + 1); else { _curIdx = -1; _sync(); } }); } function _play(i) { const a = _audio(); - const songs = _songs(); - if (!a || !songs[i]) return; + if (!a || !SONGS[i]) return; if (i === _curIdx && !a.paused) { a.pause(); return; } // aktiven Song pausieren _curIdx = i; - a.src = songs[i].file; + a.src = SONGS[i].file; a.play().catch(() => {}); _markHeard(); _sync(); @@ -2024,67 +2006,39 @@ window.Worlds = (() => { function _closeAlbum() { document.getElementById('album-modal')?.remove(); } - // Sprache wechseln: aktuelle Wiedergabe stoppen (andere Datei) und Liste neu zeichnen. - function _setLang(l) { - if (l === _lang || !EN_READY) return; - _lang = l; - try { localStorage.setItem(LANG_KEY, l); } catch (_) {} - const a = _audio(); if (a) a.pause(); - _curIdx = -1; - _fillAlbum(); - _sync(); - } - - // Inhalt des Sheets (neu) rendern + innere Controls binden — auch bei Sprachwechsel. - function _fillAlbum() { - const sheet = document.querySelector('#album-modal .album-sheet'); - if (!sheet) return; - const songs = _songs(); - const en = _lang === 'en'; - sheet.innerHTML = ` -
-
-
${en ? 'Ban Yaro — The Album' : 'Ban Yaro — das Album'}
-
${songs.length} ${en ? 'songs · homemade' : 'Songs · selbst gemacht'} 🎸
-
-
- ${EN_READY ? ` -
- - -
` : ''} - -
-
-
- ${songs.map((s, i) => ` -
- - - ${_esc(s.title)} - ${_esc(s.sub)} - -
`).join('')} -
`; - sheet.querySelector('.album-close').addEventListener('click', _closeAlbum); - sheet.querySelectorAll('.album-lang-btn').forEach(b => - b.addEventListener('click', () => _setLang(b.dataset.lang))); - sheet.querySelectorAll('.album-song').forEach(row => { - const i = parseInt(row.dataset.i, 10); - row.addEventListener('click', () => _play(i)); - row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _play(i); } }); - }); - } - function openAlbum() { _markHeard(); if (document.getElementById('album-modal')) return; const ov = document.createElement('div'); ov.id = 'album-modal'; - ov.innerHTML = `
`; + ov.innerHTML = ` +
+
+
+
Ban Yaro — das Album
+
${SONGS.length} Songs · selbst gemacht 🎸
+
+ +
+
+ ${SONGS.map((s, i) => ` +
+ + + ${_esc(s.title)} + ${_esc(s.sub)} + +
`).join('')} +
+
`; document.body.appendChild(ov); ov.addEventListener('click', e => { if (e.target === ov) _closeAlbum(); }); - _fillAlbum(); + ov.querySelector('.album-close').addEventListener('click', _closeAlbum); + ov.querySelectorAll('.album-song').forEach(row => { + const i = parseInt(row.dataset.i, 10); + row.addEventListener('click', () => _play(i)); + row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _play(i); } }); + }); _sync(); } @@ -2107,7 +2061,7 @@ window.Worlds = (() => { updateButton(); } - return { heard, toggle: openAlbum, updateButton, initWelt, count: SONGS_DE.length }; + return { heard, toggle: openAlbum, updateButton, initWelt, count: SONGS.length }; })(); function _renderWelt() { diff --git a/backend/static/landing.html b/backend/static/landing.html index f190627..e92f60f 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz diff --git a/backend/static/sounds/amy-en.mp3 b/backend/static/sounds/amy-en.mp3 deleted file mode 100644 index 2d42126..0000000 Binary files a/backend/static/sounds/amy-en.mp3 and /dev/null differ diff --git a/backend/static/sounds/at-the-groomers-en.mp3 b/backend/static/sounds/at-the-groomers-en.mp3 deleted file mode 100644 index 0d1fe2f..0000000 Binary files a/backend/static/sounds/at-the-groomers-en.mp3 and /dev/null differ diff --git a/backend/static/sounds/ban-yaro-blues-en.mp3 b/backend/static/sounds/ban-yaro-blues-en.mp3 deleted file mode 100644 index 28811ca..0000000 Binary files a/backend/static/sounds/ban-yaro-blues-en.mp3 and /dev/null differ diff --git a/backend/static/sounds/ban-yaro-mobil-en.mp3 b/backend/static/sounds/ban-yaro-mobil-en.mp3 deleted file mode 100644 index 3f4fc3f..0000000 Binary files a/backend/static/sounds/ban-yaro-mobil-en.mp3 and /dev/null differ diff --git a/backend/static/sounds/best-friend-en.mp3 b/backend/static/sounds/best-friend-en.mp3 deleted file mode 100644 index 314e45a..0000000 Binary files a/backend/static/sounds/best-friend-en.mp3 and /dev/null differ diff --git a/backend/static/sounds/splash-en.mp3 b/backend/static/sounds/splash-en.mp3 deleted file mode 100644 index e732615..0000000 Binary files a/backend/static/sounds/splash-en.mp3 and /dev/null differ diff --git a/backend/static/sounds/treat-paradise-en.mp3 b/backend/static/sounds/treat-paradise-en.mp3 deleted file mode 100644 index c08deaa..0000000 Binary files a/backend/static/sounds/treat-paradise-en.mp3 and /dev/null differ diff --git a/backend/static/sw.js b/backend/static/sw.js index 30f9606..3eca1b4 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1303'; +const VER = '1300'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten diff --git a/promotion/ban-yaro-blues/platsch-reel-shotlist.md b/promotion/ban-yaro-blues/platsch-reel-shotlist.md deleted file mode 100644 index 2b5f50f..0000000 --- a/promotion/ban-yaro-blues/platsch-reel-shotlist.md +++ /dev/null @@ -1,80 +0,0 @@ -# Platsch! — Reel-Shotlist (Pilot-Clip) - -**Zweck:** Erster Social-Pilot der Song-Reel-Serie (siehe `MARKETING.md` → „Song-Reel / Short-Video"). -**Ziel-Kennzahl:** App-Installs pro Clip (nicht Tantiemen) → ref-Link unten. -**Song:** `Platsch Nassrein.mp3` / live als `/sounds/platsch.mp3`. Funk-Blues, ~96 BPM, A-Moll, bass-getrieben, sommerlich. -**Format:** vertikal 9:16, ~16–20 s, Ton **kommerziell frei** (Suno Pro). -**Saison:** Sommer = JETZT ideal — zeitnah raus. - ---- - -## Audio-Segment - -Nicht den ganzen Song — nur den **Refrain** als Clip-Audio (sofortiger Hook): - -> Platsch! — und ich bin drin, -> Platsch! — kein Halten mehr. -> Schütteln, lachen, nochmal rein — -> Wasser, ich komm rein! - -Danach 1–2 s Bass-Groove als Auslauf für die Endcard. -**Beat-Raster:** 96 BPM → 1 Beat = 0,625 s, 1 Takt = 2,5 s. In CapCut **Beat-Marker** setzen; die beiden **„Platsch!"** sind die wichtigsten Sync-Punkte (Wasser-Aufprall genau aufs Wort). - ---- - -## Teil A — Was filmen (Roh-Shots mit Ban & Yaro) - -In **Slow-Mo (120/240 fps)** filmen, damit Spritzer episch werden. Möglichst direkt **vertikal** oder mit Rand. - -| # | Shot | Hinweis | -|---|---|---| -| A1 | **Sprung ins Wasser**, Seitenprofil | DER Schlüssel-Shot, Slow-Mo, großer Spritzer | -| A2 | Auftauchen / Paddeln, Nahaufnahme Gesicht | glückliche Augen | -| A3 | Rennen durchs flache Wasser **auf die Kamera zu** | niedrige Perspektive, Spray | -| A4 | **Schüttel-Shot**, Slow-Mo, Gegenlicht | Tropfen fliegen radial — sehr fotogen | -| A5 | Pfote schlägt aufs Wasser / Sprung in Pfütze | Detail, anderer Winkel | -| A6 | Zufrieden, tropfnass, **Blick in die Kamera** | ruhiger Schluss-Shot | -| B | Schnittbilder | Wasserspiegelung, Ufer, Sommerstimmung | - ---- - -## Teil B — Schnitt-Timeline (auf den Beat) - -| Clip-Zeit | Audio-Cue | Shot | On-Screen-Text | Schnitt | -|---|---|---|---|---| -| 0,0–1,3 s | **„Platsch!"** (1) | A1 Sprung, Aufprall **genau aufs Wort** | **„PLATSCH!"** groß, zentriert | 0,2 s Freeze auf Impact-Frame | -| 1,3–2,6 s | „und ich bin drin," | A2 Auftauchen/Paddeln | Lyric-Untertitel unten | Cut auf Beat | -| 2,6–3,9 s | **„Platsch!"** (2) | A5 Pfote/Pfütze, anderer Winkel | **„PLATSCH!"** Pop | Cut aufs Wort | -| 3,9–5,2 s | „kein Halten mehr." | A3 Rennen auf Kamera zu | Untertitel | Beat-Cut | -| 5,2–7,0 s | „Schütteln, lachen, nochmal rein —" | A4 Schüttel-Slow-Mo, Gegenlicht | „Schütteln, lachen…" | Slow-Mo rein auf „Schütteln" | -| 7,0–9,0 s | „Wasser, ich komm rein!" | A1 Sprung **nochmal** (Thema „nochmal rein") | Untertitel | Beat-Cut | -| 9,0–11 s | Bass-Groove | Mikro-Montage: 3× ~0,5 s (Plansch/Grinsen/Schütteln) | — | schnelle Beat-Cuts | -| 11–13 s | „Platsch…" / Bass-Fade | A6 zufrieden, Blick in Kamera | — | ruhiger Ausklang | -| 13–16 s | Bass-Fade | **Endcard** (Logo-BG oder über letztem Frame) | App-Logo + „Ban Yaro — Gratis-App für Hundemenschen" + **banyaro.app** + 🐾 | sanfter Fade-in | - ---- - -## Untertitel & Sound-off - -Die meisten scrollen **stumm** → animierte Lyric-Untertitel sind Pflicht (CapCut Auto-Captions, dann je Zeile timen). „Platsch!" größer/farbig als Akzent. - -## CTA & Tracking - -- Endcard-Link & Bio/Pinned: **`banyaro.app/?ref=reel-ig`** (bzw. `-tt`, `-yt` je Kanal) — zählbar via „Messung" in `MARKETING.md`. -- Kanäle: Instagram Reels · TikTok · YouTube Shorts (gleicher Clip) + lokale FB-Gruppen (EBE). - -## Caption-Vorschlag - -> Wenn's heiß ist, gibt's für Ban Yaro nur eine Lösung 💦 **Platsch!** 🐾 -> Song aus unserem selbstgemachten Album 🎸 -> Mehr Hundeliebe (Gratis-App) → banyaro.app -> #hundeliebe #dogsofinstagram #hundevideo #sommer #ebersberg - -## Tools / Workflow - -1. Roh-Shots A1–A6 sammeln/filmen (Slow-Mo!). -2. CapCut: Audio rein → Beat-Marker → Shots auf die Marker ziehen. -3. Auto-Captions → Lyrics anpassen, „Platsch!" betonen. -4. Endcard + ref-Link. -5. Export 9:16, 1080p → IG/TikTok/Shorts + FB. -6. Reaktion messen, **dann** Batch (Leckerli-Paradies, Beim Friseur, Ban Yaro Mobil). diff --git a/tests/test_forum_pinning.py b/tests/test_forum_pinning.py deleted file mode 100644 index d5c45f5..0000000 --- a/tests/test_forum_pinning.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Forum-Anpinnen: Berechtigung (Admin/Moderator) + Scope-Sortierung. - -Deckt zwei neue Verhalten ab: -1. Admins OHNE is_moderator-Flag dürfen anpinnen (Rolle zählt, nicht nur Flag). -2. pin_scope='global' hält oben in JEDER Ansicht, pin_scope='kategorie' nur in - der gefilterten Kategorie (nicht in der "Alle"-Liste). - -Hinweise zu den Fixtures/Route-Constraints: -- `admin`-Fixture setzt rolle='admin' direkt in der DB, OHNE is_moderator. -- forum.create_thread hat 30s-Cooldown + 5-Threads/Stunde — daher gestaffelte - client_time. Texte sind eindeutig (token_hex) gegen den Duplikat-Check. -- Kategorie 'tauschboerse' isoliert die Sortier-Tests von anderen Suiten; - Assertions vergleichen nur die RELATIVE Reihenfolge der eigenen Thread-IDs. -""" - -from __future__ import annotations - -import secrets - - -def _mk_thread(client, headers, kategorie, titel, client_time): - r = client.post( - "/api/forum/threads", - headers=headers, - json={ - "kategorie": kategorie, - "titel": titel, - "text": f"{titel}: Hallo zusammen, ein Testbeitrag {secrets.token_hex(8)}.", - "client_time": client_time, - }, - ) - assert r.status_code == 201, f"create_thread failed: {r.status_code} {r.text}" - return r.json() - - -class TestForumPinPermission: - def test_admin_without_moderator_flag_can_pin(self, client, admin): - """rolle='admin' ohne is_moderator darf anpinnen (Kernursache-Fix).""" - t = _mk_thread(client, admin["headers"], "tauschboerse", "Admin Pin", "2026-06-18T08:00:00") - r = client.patch( - f"/api/forum/threads/{t['id']}", - headers=admin["headers"], - json={"is_pinned": 1, "pin_scope": "global"}, - ) - assert r.status_code == 200, f"{r.status_code} {r.text}" - body = r.json() - assert body["is_pinned"] == 1 - assert body["pin_scope"] == "global" - - def test_normal_user_cannot_pin(self, client, user): - """Normaler User → 403, Anpinnen bleibt Admin/Mod vorbehalten.""" - t = _mk_thread(client, user["headers"], "tauschboerse", "User Pin", "2026-06-18T08:00:00") - r = client.patch( - f"/api/forum/threads/{t['id']}", - headers=user["headers"], - json={"is_pinned": 1, "pin_scope": "global"}, - ) - assert r.status_code == 403 - - def test_invalid_pin_scope_rejected(self, client, admin): - """Ungültiger pin_scope → 400.""" - t = _mk_thread(client, admin["headers"], "tauschboerse", "Bad Scope", "2026-06-18T08:00:00") - r = client.patch( - f"/api/forum/threads/{t['id']}", - headers=admin["headers"], - json={"is_pinned": 1, "pin_scope": "bogus"}, - ) - assert r.status_code == 400 - - -class TestForumPinScopeSorting: - def test_scope_controls_where_thread_floats(self, client, admin): - h = admin["headers"] - t1 = _mk_thread(client, h, "tauschboerse", "Erster", "2026-06-18T09:00:00") - t2 = _mk_thread(client, h, "tauschboerse", "Zweiter", "2026-06-18T09:20:00") - t3 = _mk_thread(client, h, "tauschboerse", "Dritter", "2026-06-18T09:40:00") - ids = {t1["id"], t2["id"], t3["id"]} - - def order(kategorie=None): - url = "/api/forum/threads?limit=200" - if kategorie: - url += f"&kategorie={kategorie}" - r = client.get(url, headers=h) - assert r.status_code == 200 - return [x["id"] for x in r.json() if x["id"] in ids] - - # Ohne Pin: neueste zuerst. - assert order() == [t3["id"], t2["id"], t1["id"]] - - # Ältesten Thread GLOBAL anpinnen → oben in "Alle" UND in der Kategorie. - r = client.patch(f"/api/forum/threads/{t1['id']}", headers=h, - json={"is_pinned": 1, "pin_scope": "global"}) - assert r.status_code == 200 - assert order()[0] == t1["id"] - assert order("tauschboerse")[0] == t1["id"] - - # Auf Themen-Pin umstellen → NUR in der Kategorie oben, in "Alle" wieder nach Datum. - r = client.patch(f"/api/forum/threads/{t1['id']}", headers=h, - json={"pin_scope": "kategorie"}) - assert r.status_code == 200 - assert order() == [t3["id"], t2["id"], t1["id"]] # "Alle": Themen-Pin zählt nicht - assert order("tauschboerse")[0] == t1["id"] # Kategorie: oben diff --git a/tools/album-build/build.sh b/tools/album-build/build.sh deleted file mode 100755 index ae94723..0000000 --- a/tools/album-build/build.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash -# Baut die zwei Album-Downloads (DE + EN) reproduzierbar: -# Cover (Foto quadr. + Wortmarke) → ID3-Tags + eingebettetes Cover → ZIP mit Liner Notes. -# Quelle: die committeten MP3s in backend/static/sounds/ + ein Hi-Res-Foto. -# Ausgabe: backend/static/downloads/ban-yaro-album-{de,en}.zip + img/banyaro/album-thumb.jpg -# Benötigt: ImageMagick (magick), ffmpeg, zip. Aufruf: make album (oder bash tools/album-build/build.sh) -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -SND="$ROOT/backend/static/sounds" -PHOTO="$ROOT/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg" -FONT="/System/Library/Fonts/Supplemental/Arial Bold.ttf" -BUILD="$ROOT/tools/album-build" -DIST="$BUILD/dist" -DL="$ROOT/backend/static/downloads" -IMG="$ROOT/backend/static/img/banyaro" - -rm -rf "$DIST"; mkdir -p "$DIST" "$DL" "$IMG" - -# --- 1) Cover (quadratischer Crop auf die Hunde + Verlauf unten + Wortmarke) --- -make_cover() { # $1=subtitle $2=outfile - magick "$PHOTO" -auto-orient -crop 2648x2648+551+0 +repage -resize 2000x2000 \ - \( -size 2000x950 gradient:none-'rgba(0,0,0,0.80)' \) -gravity south -compose over -composite \ - -gravity south -font "$FONT" -kerning 10 \ - -fill 'rgba(0,0,0,0.55)' -pointsize 168 -annotate +4+254 "BAN YARO" \ - -fill white -pointsize 168 -annotate +0+250 "BAN YARO" \ - -kerning 7 -fill 'rgba(255,255,255,0.92)' -pointsize 60 -annotate +0+160 "$1" \ - -quality 90 "$2" -} -make_cover "DAS ALBUM · 7 SONGS" "$DIST/cover-de.jpg" -make_cover "THE ALBUM · 7 SONGS" "$DIST/cover-en.jpg" -# Eingebettetes Albumart kleiner halten (sonst blähen sich die MP3s auf) -magick "$DIST/cover-de.jpg" -resize 800x800 -quality 85 "$DIST/art-de.jpg" -magick "$DIST/cover-en.jpg" -resize 800x800 -quality 85 "$DIST/art-en.jpg" -# Neutrales Thumbnail (ohne Text) für die Profil-Karte — der Titel steht dort im HTML -magick "$PHOTO" -auto-orient -crop 2648x2648+551+0 +repage -resize 600x600 -quality 85 "$IMG/album-thumb.jpg" - -# --- 2)+3) pro Sprache: taggen + zippen --- -# Zeilenformat: "quelldatei(ohne .mp3)|Titel|Untertitel" -DE_TRACKS=( - "ban-yaro-blues|Ban Yaro Blues|Die Hymne" - "ban-yaro-mobil|Ban Yaro Mobil|Erste Fahrt im Anhänger" - "amy|Amy|Eine Liebesromanze" - "beim-friseur|Beim Friseur|Halbes Fell, Energie pur" - "leckerli-paradies|Leckerli-Paradies|Voller Napf, volles Glück" - "platsch|Platsch!|Ab ins kühle Nass" - "bester-freund|Bester Freund|Du und ich" -) -EN_TRACKS=( - "ban-yaro-blues-en|Ban Yaro Blues|The anthem" - "ban-yaro-mobil-en|Ban Yaro Mobile|First ride in the trailer" - "amy-en|Amy|A love duet" - "at-the-groomers-en|At the Groomer's|Half the fur, all the energy" - "treat-paradise-en|Treat Paradise|Full bowl, full heart" - "splash-en|Splash!|Into the cool water" - "best-friend-en|Best Friend|You and me" -) - -zip_album() { # $1=lang $2=AlbumName(ID3, Em-Dash ok) $3=Ordner(ASCII) $4=art $5=liner shift 5; rest=tracks - local lang="$1" album="$2" fname="$3" art="$4" liner="$5"; shift 5 - local folder="$DIST/$fname"; rm -rf "$folder"; mkdir -p "$folder" - if [ "$lang" = "de" ]; then cp "$liner" "$folder/LIESMICH.txt"; else cp "$liner" "$folder/README.txt"; fi - cp "$art" "$folder/cover.jpg" - local copyr - if [ "$lang" = "de" ]; then - copyr="© 2026 Ban Yaro — Alle Rechte vorbehalten. Kommerzielle Nutzung nur nach Absprache (banyaro.app)." - else - copyr="© 2026 Ban Yaro — All rights reserved. Commercial use only by prior arrangement (banyaro.app)." - fi - local n=0 line src title sub nn - for line in "$@"; do - n=$((n+1)); nn=$(printf "%02d" "$n") - IFS='|' read -r src title sub <<<"$line" - ffmpeg -y -loglevel error -i "$SND/$src.mp3" -i "$art" \ - -map 0:a -map 1:v -c copy -id3v2_version 3 \ - -metadata title="$title" -metadata artist="Ban Yaro" \ - -metadata album="$album" -metadata album_artist="Ban Yaro" \ - -metadata track="$n/7" -metadata date="2026" -metadata genre="Blues" \ - -metadata copyright="$copyr" -metadata comment="$sub" \ - -disposition:v:0 attached_pic \ - "$folder/$nn $title.mp3" - done - ( cd "$DIST" && rm -f "$DL/ban-yaro-album-$lang.zip" && zip -r -X -q "$DL/ban-yaro-album-$lang.zip" "$fname" ) - echo " ✓ $fname → ban-yaro-album-$lang.zip ($(du -h "$DL/ban-yaro-album-$lang.zip" | cut -f1))" -} - -# LIESMICH.txt für DE, README.txt für EN -cp "$BUILD/liner-de.txt" "$DIST/_liner-de.txt" -cp "$BUILD/liner-en.txt" "$DIST/_liner-en.txt" -zip_album "de" "Ban Yaro — Das Album" "Ban Yaro - Das Album" "$DIST/art-de.jpg" "$DIST/_liner-de.txt" "${DE_TRACKS[@]}" -zip_album "en" "Ban Yaro — The Album" "Ban Yaro - The Album" "$DIST/art-en.jpg" "$DIST/_liner-en.txt" "${EN_TRACKS[@]}" - -echo "Fertig. Downloads in backend/static/downloads/, Thumbnail in img/banyaro/album-thumb.jpg" diff --git a/tools/album-build/liner-de.txt b/tools/album-build/liner-de.txt deleted file mode 100644 index f4bbeba..0000000 --- a/tools/album-build/liner-de.txt +++ /dev/null @@ -1,32 +0,0 @@ -================================================== - BAN YARO — DAS ALBUM - 7 Songs -================================================== - -Sieben Lieder über einen Hund namens Ban Yaro: -übers Gassigehen bei jedem Wetter, die erste Fahrt -im Anhänger, die große Liebe, den Friseur, den -vollen Napf, das kühle Wasser — und den besten -Freund, den ein Mensch haben kann. - -Selbst gemacht, mit viel Herz. Aus der Ban-Yaro-App. - --------------------------------------------------- -TRACKLIST --------------------------------------------------- - 1. Ban Yaro Blues Die Hymne - 2. Ban Yaro Mobil Erste Fahrt im Anhänger - 3. Amy Eine Liebesromanze - 4. Beim Friseur Halbes Fell, Energie pur - 5. Leckerli-Paradies Voller Napf, volles Glück - 6. Platsch! Ab ins kühle Nass - 7. Bester Freund Du und ich - --------------------------------------------------- -© 2026 Ban Yaro — Musik und Texte. Alle Rechte vorbehalten. - -Privat behalten und teilen ist ausdrücklich erwünscht. -Kommerzielle Nutzung nur nach Absprache: banyaro.app - -Viel Freude beim Hören und einen schönen -Gassi-Gang! Wuff. 🐾 diff --git a/tools/album-build/liner-en.txt b/tools/album-build/liner-en.txt deleted file mode 100644 index d04e18f..0000000 --- a/tools/album-build/liner-en.txt +++ /dev/null @@ -1,32 +0,0 @@ -================================================== - BAN YARO — THE ALBUM - 7 Songs -================================================== - -Seven songs about a dog named Ban Yaro: -about walks in every kind of weather, the first -ride in the trailer, falling in love, the groomer, -a full bowl, cool water — and the best friend a -human could ever ask for. - -Homemade, with all our heart. From the Ban Yaro app. - --------------------------------------------------- -TRACKLIST --------------------------------------------------- - 1. Ban Yaro Blues The anthem - 2. Ban Yaro Mobile First ride in the trailer - 3. Amy A love duet - 4. At the Groomer's Half the fur, all the energy - 5. Treat Paradise Full bowl, full heart - 6. Splash! Into the cool water - 7. Best Friend You and me - --------------------------------------------------- -© 2026 Ban Yaro — music and lyrics. All rights reserved. - -Keeping and sharing privately is expressly welcome. -Commercial use only by prior arrangement: banyaro.app - -Enjoy the music, and have a wonderful walk! -Woof. 🐾