diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/AGENTS.md b/AGENTS.md index e459e6b..632bcf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ Netcup-Server erreichbar per SSH-Key: | Open WebUI | https://ui.datenhimmel.work | /opt/openwebui (docker compose) | | Ollama | localhost:11434 | systemd service, user: ollama | | SearchTool | https://search.datenhimmel.work | /opt/search-tool (docker compose) | +| Letta | http://127.0.0.1:8283 | /opt/letta (docker compose) | ## OpenCode API-Zugang @@ -147,3 +148,31 @@ ssh hermes@185.162.249.159 '/home/hermes/venv/bin/hermes config edit' - git (Gitea system user) - ollama (Ollama system user) - hermes (Hermes Agent SSH sandbox, Workspace: /home/hermes/workspace) + +## Letta (Memory-Agent-Plattform) + +Letta (ehemals MemGPT) ist eine Plattform für zustandsbehaftete AI-Agenten mit Langzeitspeicher. + +- **URL:** http://127.0.0.1:8283 (nur lokal, kein externer Zugriff) +- **Pfad:** /opt/letta (docker compose) +- **Repo:** git@185.162.249.159:danielkrause/letta-server.git +- **API-Docs:** http://127.0.0.1:8283/docs (Swagger UI) +- **Health:** GET /v1/health/ → `{"version":"0.16.8","status":"ok"}` +- **DB:** Eigener PostgreSQL+pgvector Container (letta-pg), persistent Volume +- **LLM Backend:** OpenWebUI (`openwebui-open-webui-1:8080/api`), gleiche Credentials wie Hermes +- **Netzwerk:** Container teilt sich `letta-net` (intern) + `openwebui_default` (für LLM-Zugriff) +- **Provider-Handle:** `openai-proxy/` (z.B. `openai-proxy/deepseek-v4-pro:cloud`) + +```bash +# Steuern +ssh root@185.162.249.159 'cd /opt/letta && docker compose up -d' +ssh root@185.162.249.159 'cd /opt/letta && docker compose down' + +# Update +ssh root@185.162.249.159 'cd /opt/letta && docker compose pull && docker compose up -d' + +# Agent erstellen (Beispiel) +curl -X POST http://127.0.0.1:8283/v1/agents/ \ + -H 'Content-Type: application/json' \ + -d '{"name":"test","model":"openai-proxy/deepseek-v4-pro:cloud"}' +``` diff --git a/asmodee_middle_earth_article.html b/asmodee_middle_earth_article.html new file mode 100644 index 0000000..41f591c --- /dev/null +++ b/asmodee_middle_earth_article.html @@ -0,0 +1,228 @@ + + + + + +Asmodee öffnet Mittelerde: Externe Publisher erhalten Zugang zur Tolkien-Lizenz + + + +
+ +

Asmodee öffnet Mittelerde: Externe Publisher erhalten Zugang zur Tolkien-Lizenz

+

21. Mai 2026 — von Hermes

+

Mit einem neuen Sublizenz-Programm vergibt Asmodee erstmals Herr-der-Ringe-Lizenzen an externe Verlage. Fünf Partner stehen fest – darunter ein umstrittener KI-Pionier und die Hoffnung auf das erste Mittelerde-Eurogame.

+ +
+ +
+

Ein Paradigmenwechsel für Tolkiens Erbe

+

Es ist eine Nachricht, die in der Brettspiel-Welt Wellen schlägt: Asmodee, der weltgrößte Brettspiel-Publisher und -Distributor, hat die erste Riege externer Partner-Publisher bekanntgegeben, die über das neu geschaffene Middle-earth Enterprises (MEE) Category Management Program Zugriff auf die Lizenz zu J.R.R. Tolkiens Mittelerde erhalten. Bislang erschienen alle neuen Herr-der-Ringe-Brettspiele ausschließlich bei Asmodee-eigenen Studios wie Fantasy Flight Games oder Z-Man Games. Mit diesem Schritt öffnet Asmodee die Lizenz nun erstmals für externe Verlage.

+ +

Die Ankündigung ist der vorläufige Höhepunkt einer Kette von Unternehmensübernahmen, die 2022 begann: Damals erwarb die Embracer Group Middle-earth Enterprises (MEE) von der Saul Zaentz Company – inklusive der weltweit exklusiven Film-, Merchandising- und Brettspiel-Rechte an Der Herr der Ringe und Der Hobbit. Der Kaufpreis wurde nicht veröffentlicht, der Abschluss erfolgte im Oktober 2022. Embracer hatte bereits im Dezember 2021 die Übernahme von Asmodee für €2,75 Milliarden angekündigt (Wikipedia) und im März 2022 abgeschlossen. Damit kamen Rechteinhaber (MEE) und größter Produzent (Asmodee) unter ein Dach – die Grundlage für das heutige Sublizenz-Modell.

+
+ +
+ +
+

Die fünf Partner der ersten Stunde

+

Die offizielle Pressemitteilung von Asmodee ist derzeit über die Corporate-Website nicht direkt erreichbar (Access Denied). Eine ausführliche Zusammenfassung der Ankündigung findet sich jedoch in einem Reddit-Thread auf r/boardgames sowie in der Branchenpublikation ICv2. Die fünf gelisteten Partner sind:

+ +

🎲 Sirius Dice

+

Bekannt für Premium-Würfel und Tabletop-Zubehör, hat das Unternehmen bereits mehrfach unter Lizenz gearbeitet – etwa für Dungeons & Dragons (Mists of Ravenloft Treasure Packs, Mai 2026). Von Sirius Dice sind Mittelerde-Themen-Würfel, Würfelbecher und Tischaccessoires zu erwarten. Dass Asmodee hier keinen Spieleverlag, sondern einen Zubehör-Hersteller als Partner nennt, zeigt: Das Category Management denkt breiter als nur Brettspiele.

+ +

🃏 Stone Blade Entertainment

+

Der Entwickler von Ascension und Shards of Infinity hat bereits ein konkretes Projekt angekündigt: Lord of the Rings: Ascension, eine thematische Version ihres erfolgreichen Deckbuilding-Spiels. Allerdings gibt es eine Kontroverse: Wie ein Community-Mitglied im Reddit-Thread anmerkt, setzt Stone Blade zunehmend auf generative KI für Artwork (u/zoso_coheed, +43 Upvotes). Für eine Lizenz mit der künstlerischen Tradition Tolkiers ein heikler Punkt.

+ +

🪑 Game Toppers

+

Der Hersteller von qualitativen Spieltischmatten und -abdeckungen wird Mittelerde-thematisierte Tischauflagen produzieren – besonders naheliegend für Spiele wie das Lord of the Rings LCG oder Journeys in Middle-earth. Die Community reagiert positiv: „A LOTR mat would be awesome", kommentiert u/Cooptah.

+ +

🎯 Play To Z

+

Das bislang unbekannteste Unternehmen in der Runde. Konkrete Pläne wurden nicht genannt. In der Community wird spekuliert, dass Play To Z ein bestehendes Spielsystem – etwa Tales of the Arabian Nights – auf Mittelerde übertragen könnte. Derzeit gibt es dazu keine offizielle Bestätigung.

+ +

🏰 Grand Gamers Guild

+

Der Verlag hinter dem hochgelobten Endeavor: Deep Sea (BGG-Rang #368) und The Artemis Project. Die Community hegt eine besondere Hoffnung: Das erste Mittelerde-Eurogame. Eurogames mit Arbeiter-Einsatz, Engine-Building und Ressourcen-Management sind in der Tolkien-Welt bislang eine Leerstelle – Grand Gamers Guild könnte sie füllen. Ein Reddit-Nutzer formuliert es so: „Would be interesting to see someone tackle a Middle Earth euro game as none exist" (Kommentar im Thread).

+
+ +
+ +
+

Die Hintergrundstory: Embracer, Asmodee und 18 Jahre bis zur Public Domain

+ +

Die strategische Dringlichkeit hinter dem Sublizenz-Programm wird klarer, wenn man die Urheberrechtslage betrachtet. J.R.R. Tolkien starb am 2. September 1973. In Großbritannien und der EU beträgt die Schutzfrist 70 Jahre nach dem Tod des Autors (Copyright, Designs and Patents Act 1988). Das bedeutet: Am 1. Januar 2044 werden The Hobbit und The Lord of the Rings als literarische Werke in diesen Jurisdiktionen gemeinfrei.

+ +
+

Wichtig zu verstehen

+

Das Urheberrecht an den Texten und die Verwertungsrechte von Middle-earth Enterprises sind nicht dasselbe. MEE besitzt vertragliche Film-, Merchandising- und Spiele-Rechte – sowie zahlreiche Markenrechte an Namen wie „Mittelerde", „Hobbit" oder „Herr der Ringe" –, die über das Erlöschen des Urheberrechts hinaus Bestand haben können. Ab 2044 darf jeder zwar die Bücher nachdrucken und übersetzen, aber noch lange nicht eigenmächtig Mittelerde-Filme oder -Brettspiele produzieren. In den USA greifen zudem andere Fristen: The Hobbit dort frühestens 2033 Public Domain, The Lord of the Rings frühestens 2050 (Copyright Term Extension Act).

+
+ +

Die 18 Jahre bis 2044 schaffen einen wirtschaftlichen Druck, die Lizenz maximal auszuschöpfen – und das Sublizenz-Modell ist das Vehikel dafür. Wie ein Reddit-Nutzer es auf den Punkt bringt: „Looks like they're really milking the fat cow as hard as they can. I think the IP goes public domain in 2044, so they're going pretty hard before time runs out." (u/Marksman1977, +3)

+ +

Hinzu kommt eine strukturelle Besonderheit: Asmodee war bereits vor der MEE-Übernahme einer der größten LOTR-Lizenznehmer mit über einem Dutzend Spielen und mehr als 100 Erweiterungen, wie die Embracer-Pressemitteilung vom August 2022 selbst festhält. Nach der Übernahme wurden Rechteinhaber und größter Lizenznehmer eins – die Geburtsstunde des Category-Management-Modells, bei dem Asmodee die Vergabe von Sublizenzen an Dritte koordiniert.

+
+ +
+ +
+

Was die Community sagt

+ +

Der Reddit-Thread auf r/boardgames (73 Upvotes, 36 Kommentare) spiegelt eine gespaltene Community. Die Reaktionen reichen von Begeisterung bis Skepsis – und die interessantesten Stimmen kommen nicht immer von den am höchsten bewerteten Kommentaren.

+ +

Die KI-Kontroverse

+

Der mit Abstand am höchsten bewertete Kommentar (+43) ist eine Warnung: „Info or reminder for those that care: Stoneblade (Ascension) is openly leaning fairly heavily into generative AI now." – u/zoso_coheed. Für viele in der Community ist die Kombination aus KI-generiertem Artwork und Tolkiens Werk ein ethischer Konflikt.

+ +

Sättigungsangst vs. Begeisterung

+

u/wallysmith127 beobachtet: „For the most part it seems like LotR games have been better-than-average (or truly great like Fate of the Fellowship) but saturation and fatigue is a potential risk now." Eine Einschätzung, die nicht unbegründet ist: Seit 2024 erschienen mit Duel for Middle Earth (ein Reskin von 7 Wonders Duel) und Fate of the Fellowship gleich zwei größere LOTR-Spiele aus Asmodee-eigenen Studios. Nun kommen fünf externe Partner hinzu.

+ +

Dem gegenüber steht u/XIIICaesar: „I know some people are tired of the LOTR license, but I feel the LOTR theme is extremely good, I prefer it to all other fantasy themes. We've been treated with games like Fate of the fellowship and Duel for Middle Earth lately; games that marry the theme and game mechanics so well. I can't wait to see what else we'll be getting in the coming years."

+ +

Das Ende des LCG

+

Ein Nebenschauplatz der Diskussion: Das Lord of the Rings: The Card Game (LCG) – einst eines der erfolgreichsten Fantasy Flight-Spiele – ist offiziell out-of-print. u/sennohki merkt an: „I highly doubt they'll do anything specifically for the LCG, considering they've recently stated it's OOP and they have no plans to print more." Ein bedeutender Einschnitt für die Fangemeinde des Spiels.

+ +

Ungewöhnliches Lizenzmodell

+

u/lilbismyfriend300 fragt: „A publisher getting a license then sub-licensing it to other publishers? Is that normal?" Die kurze Antwort: Nein. Dass ein Publisher (Asmodee) die Master-Lizenz hält und Sublizenzen an Wettbewerber vergibt, ist im Brettspielbereich ungewöhnlich – aber bei Mega-IPs wie Mittelerde oder Star Wars wird es zunehmend zum Modell.

+ +

Wunschliste der Community

+

Die Sehnsüchte der Fans werden in den Kommentaren deutlich: „Restoration Games and Unmatched please..." (u/shortielah) und die Idee einer LOTR-Version von Zombicide (u/Physical_Quit_7100: „Instead of zombies, just have orcs, goblins, trolls, corrupted men.") zeigen die Erwartungshaltung: Die Community will Qualität, aber auch Kreativität und thematische Tiefe – nicht bloß Lizenz-Aufgüsse.

+
+ +
+ +
+

Einordnung und Ausblick

+ +

Das Sublizenz-Programm von Asmodee ist strategisch klug. Es verteilt das Produktionsrisiko, erhöht die Marktdurchdringung und maximiert den Ertrag aus der Lizenz vor dem Public-Domain-Stichtag 2044. Gleichzeitig birgt es Risiken:

+ +
    +
  • Sättigung: Fünf zusätzliche Publisher bedeuten fünf zusätzliche Produkt-Roadmaps. Die Gefahr, dass der Markt mit LOTR-Produkten überflutet wird, ist real.
  • +
  • Qualitätskontrolle: Asmodee muss sicherstellen, dass Sublizenznehmer den Qualitätsstandard halten, den die Marke verlangt – insbesondere bei sensiblen Themen wie KI-generiertem Artwork.
  • +
  • Präzedenzfall: Falls das Modell erfolgreich ist, könnten andere IP-Inhaber (Disney für Star Wars, Marvel) ähnliche Sublizenz-Programme aufsetzen – mit weitreichenden Folgen für die gesamte Branche.
  • +
+ +

Ob das Modell aufgeht, wird sich in den kommenden 12 bis 24 Monaten zeigen, wenn die ersten Produkte der Sublizenz-Partner erscheinen. Für die Brettspiel-Community bleibt zu hoffen, dass die Qualität der Spiele mit der strategischen Vision der Lizenzgeber mithalten kann.

+
+ +
+ +
+

Quellen

+
    +
  1. Embracer Group: Pressemitteilung zum Erwerb von Middle-earth Enterprises (18. August 2022) — Primärquelle (Archiv)
  2. +
  3. Asmodee Corporate: Offizielle Pressemitteilung zur Partner-Ankündigung (Mai 2026) — Derzeit nicht direkt abrufbar (Access Denied)
  4. +
  5. Reddit r/boardgames: Diskussions-Thread zur Ankündigung (Mai 2026) — Enthält Zusammenfassung der PM und Community-Reaktionen
  6. +
  7. ICv2: Berichterstattung zur Ankündigung (Mai 2026) — Branchen-Publikation
  8. +
  9. Wikipedia: Middle-earth Enterprises — Hintergrund zu Geschichte und Übernahme
  10. +
  11. Wikipedia: Embracer Group — Konzernstruktur und Übernahmen
  12. +
  13. Wikipedia: Asmodee — Unternehmensgeschichte, Übernahme durch Embracer
  14. +
  15. Wikipedia: J.R.R. Tolkien — Todesdatum (2. September 1973)
  16. +
  17. Wikipedia: Copyright law of the United Kingdom — Schutzfrist Leben + 70 Jahre
  18. +
  19. Wikipedia: Copyright law of the United States — Schutzfrist 95 Jahre ab Veröffentlichung
  20. +
  21. Middle-earth Enterprises: Offizielle Website (Archiv, Dezember 2025) — Bestätigt Übernahme im Oktober 2022
  22. +
  23. BoardGameGeek: The Lord of the Rings – Duel for Middle Earth
  24. +
  25. Fantasy Flight Games — Asmodee-eigenes Studio, Publisher des LOTR LCG und Journeys in Middle-earth
  26. +
+
+ +

Dieser Artikel wurde am 21. Mai 2026 verfasst. Alle Informationen basieren auf öffentlich zugänglichen Quellen. Die offizielle Asmodee-Pressemitteilung war zum Zeitpunkt der Recherche nicht direkt abrufbar; die Angaben zu den Partnern basieren auf der Zusammenfassung im Reddit-Thread und der ICv2-Berichterstattung.

+ +
+ + diff --git a/asmodee_middle_earth_article_v2.html b/asmodee_middle_earth_article_v2.html new file mode 100644 index 0000000..2b077b1 --- /dev/null +++ b/asmodee_middle_earth_article_v2.html @@ -0,0 +1,203 @@ + + + + + +Asmodee öffnet Mittelerde: Externe Publisher erhalten Zugang zur Tolkien-Lizenz + + + +
+ +

Asmodee öffnet Mittelerde: Externe Publisher erhalten Zugang zur Tolkien-Lizenz

+

21. Mai 2026 — von Hermes

+

Mit einem neuen Sublizenz-Programm vergibt Asmodee erstmals Herr-der-Ringe-Lizenzen an externe Verlage. Fünf Partner stehen fest – darunter ein umstrittener KI-Pionier und die Hoffnung auf das erste Mittelerde-Eurogame.

+ +
+ +
+

Ein Paradigmenwechsel für Tolkiens Erbe

+

Es ist eine Nachricht, die in der Brettspiel-Welt Wellen schlägt: Asmodee, der weltgrößte Brettspiel-Publisher und -Distributor, hat am 12. Mai 2026 die erste Riege externer Partner-Publisher bekanntgegeben, die über das neu geschaffene Middle-earth Enterprises (MEE) Category Management Program Zugriff auf die Lizenz zu J.R.R. Tolkiens Mittelerde erhalten. Bislang erschienen alle neuen Herr-der-Ringe-Brettspiele ausschließlich bei Asmodee-eigenen Studios wie Fantasy Flight Games oder Z-Man Games. Mit diesem Schritt öffnet Asmodee die Lizenz nun erstmals für externe Verlage.

+ +

Die Ankündigung ist der vorläufige Höhepunkt einer Kette von Unternehmensübernahmen, die 2022 begann: Damals erwarb die Embracer Group Middle-earth Enterprises (MEE) von der Saul Zaentz Company – inklusive der weltweit exklusiven Film-, Merchandising- und Brettspiel-Rechte an Der Herr der Ringe und Der Hobbit. Der Kaufpreis wurde nicht veröffentlicht, der Abschluss erfolgte im Oktober 2022. Embracer hatte bereits im Dezember 2021 die Übernahme von Asmodee für €2,75 Milliarden angekündigt (Wikipedia) und im März 2022 abgeschlossen. Damit kamen Rechteinhaber (MEE) und größter Produzent (Asmodee) unter ein Dach – die Grundlage für das heutige Sublizenz-Modell.

+ +
+„The Lord of the Rings ist wirklich ein besonderes Eigentum, eines, das das Wort 'Fantasy' für Generationen definiert hat. Produkte für diese Fans zu machen ist nicht nur eine Gelegenheit, sondern auch eine Ehre und eine große Verantwortung." +— Luke Peterschmidt, Head of Active Category Management, Asmodee (Pressemitteilung, 12. Mai 2026) +
+
+ +
+ +
+

Die fünf Partner der ersten Stunde

+

In der offiziellen Pressemitteilung von Asmodee werden die fünf Partner namentlich genannt und mit eigenen „About"-Profilen vorgestellt. Die Ankündigung stammt aus Boulogne-Billancourt, Frankreich, dem operativen Hauptsitz von Asmodee.

+ +
+„Es mag den Einen Ring geben, der sie alle regiert, aber es braucht viele Verlage, um die Spielbedürfnisse der Herr-der-Ringe-Fans zu erfüllen, und ich bin sicher, jeder unserer Partner wird seinen Teil in dieser Quest leisten." +— Luke Peterschmidt, Head of Active Category Management, Asmodee +
+ +

🎲 Sirius Dice

+

Gegründet 2018, ansässig in Brooklyn, New York. Sirius Dice entwirft Premium-Würfel, Tabletop-Zubehör und lizenzierte Gaming-Produkte. Das Unternehmen wurde von Lax Chandra und Tina Trenkler gegründet und ist bekannt für Handwerkskunst und Kooperationen im Hobby-Gaming-Bereich. Sirius Dice arbeitet bereits mit Distributoren, Händlern und Lizenzgebern zusammen, um Produkte zu schaffen, die Spieler näher an die Welten bringen, die sie am Tisch erkunden (siriusdice.com).

+ +

🃏 Stone Blade Entertainment

+

Ein branchenführender Publisher von Strategiespielen, darunter Ascension – das genredefinierende Deckbuilding-Spiel. Stone Blade beschäftigt laut eigener Darstellung „die weltbesten professionellen Spieldesigner, Spieler und Entwickler", um leicht erlernbare und unterhaltsame Spiele zu kreieren (stoneblade.com). Geplantes Projekt: Lord of the Rings: Ascension, eine thematische Version ihres erfolgreichen Systems.

+ +

Kontroverse: Wie ein Community-Mitglied im Reddit-Thread anmerkt, setzt Stone Blade zunehmend auf generative KI für Artwork (u/zoso_coheed, +43 Upvotes). Für eine Lizenz mit der künstlerischen Tradition Tolkiers ein heikler Punkt.

+ +

🪑 Game Toppers

+

Gestartet 2017 mit dem Ziel, Premium-Spieltische zu bauen, die jeden Tisch in eine hochwertige Gaming-Lösung verwandeln. Das Unternehmen hat laut eigenen Angaben über 5.000 Kunden weltweit beliefert und bietet neben Tischen auch Bein-Kits, Tischabdeckungen, Becherhalter und Zubehör. Game Toppers ist bekannt für premium, doppelt genähte Matten mit hochauflösender Grafik von Künstlern wie Vincent Dutrait und Ryan Laukat (gametoppers.com).

+ +

🎯 Play To Z

+

Gegründet von Veteranen der Hobby-Gaming-Branche: Publisher, Designer und Content-Creator. Präsident ist Zev Shlasinger, der Gründer von Z-Man Games, der in seiner über 25-jährigen Karriere mehr als 400 Spiele veröffentlicht hat. Die Mission: „Games For All Gamers". Zu den bisherigen Veröffentlichungen zählen 23 Knives, Ascending Empires: Zenith Edition, Animal Rescue Team, Soothsayers und Tales of the Arabian Nights – 40th Anniversary Edition.

+ +

🏰 Grand Gamers Guild

+

Beschreibt sich selbst als „größter der kleinen Indie-Publisher" mit einem Katalog, der sich weigert, in eine Schublade zu passen. Von der ozean-fokussierten Strategie und wissenschaftlichen Fundierung von Endeavor: Deep Sea über das Naturschutz-Koop Endangered bis zum erfinderischen Minimalismus von 18Escape kuratiert das Unternehmen eine Auswahl, aus der sich ein kompletter Spieleabend zusammenstellen lässt. Die Philosophie: Originalität, Substanz und Spiele, die man immer wieder auf den Tisch bringt.

+ +

Die Community hegt eine besondere Hoffnung: Das erste Mittelerde-Eurogame. Eurogames mit Arbeiter-Einsatz, Engine-Building und Ressourcen-Management sind in der Tolkien-Welt bislang eine Leerstelle – Grand Gamers Guild könnte sie füllen.

+
+ +
+ +
+

Das Category-Management-Modell: Wie es funktioniert

+ +

Asmodee agiert als exklusiver Anbieter von Category-Management-Dienstleistungen für Middle-earth Enterprises. In der Pressemitteilung heißt es:

+ +
+„Als exklusiver Anbieter von Category-Management-Dienstleistungen für Middle-earth überwacht Asmodee die Strategie in enger Zusammenarbeit mit Middle-earth Enterprises und führt die Entwicklung der Tabletop-Palette an. Das Ziel ist es, Mittelerde-Fans durch ein kohärentes Portfolio über alle Genres und Zielgruppen hinweg das bestmögliche Tabletop-Spielerlebnis zu bieten und gleichzeitig die Präsenz von Middle-earth Enterprises in der Branche auszubauen." +— Asmodee Pressemitteilung, 12. Mai 2026 +
+ +

Verlage von Brettspielen und Gaming-Zubehör, die Produkte aus der Welt von Mittelerde entwickeln möchten, sind eingeladen, Asmodee unter METTGlicensing@asmodee.com zu kontaktieren. Jeder Partner wird zu gegebener Zeit eigene Ankündigungen über seine Pläne für Mittelerde-Produkte machen und Details zu lokalen oder globalen Vertriebspartnern nennen.

+ +
+„Wir freuen uns sehr, die Sorgfalt und Verantwortung zu sehen, die Asmodee in eine mehrjährige Strategie für Mittelerde-Spiele einbringt – eine Zukunft, die nicht nur Spiele und Zubehör von Asmodee umfasst, sondern auch von anderen Verlagen." +— Jay Komas, General Manager, Middle-earth Enterprises +
+
+ +
+ +
+

Die Hintergrundstory: Embracer, Asmodee und 18 Jahre bis zur Public Domain

+ +

Die strategische Dringlichkeit hinter dem Sublizenz-Programm wird klarer, wenn man die Urheberrechtslage betrachtet. J.R.R. Tolkien starb am 2. September 1973. In Großbritannien und der EU beträgt die Schutzfrist 70 Jahre nach dem Tod des Autors (Copyright, Designs and Patents Act 1988). Das bedeutet: Am 1. Januar 2044 werden The Hobbit und The Lord of the Rings als literarische Werke in diesen Jurisdiktionen gemeinfrei.

+ +
+

Wichtig zu verstehen

+

Das Urheberrecht an den Texten und die Verwertungsrechte von Middle-earth Enterprises sind nicht dasselbe. MEE besitzt vertragliche Film-, Merchandising- und Spiele-Rechte – sowie zahlreiche Markenrechte an Namen wie „Mittelerde", „Hobbit" oder „Herr der Ringe" –, die über das Erlöschen des Urheberrechts hinaus Bestand haben können. Ab 2044 darf jeder zwar die Bücher nachdrucken und übersetzen, aber noch lange nicht eigenmächtig Mittelerde-Filme oder -Brettspiele produzieren. In den USA greifen zudem andere Fristen: The Hobbit dort frühestens 2033 Public Domain, The Lord of the Rings frühestens 2050 (Copyright Term Extension Act).

+
+ +

Die 18 Jahre bis 2044 schaffen einen wirtschaftlichen Druck, die Lizenz maximal auszuschöpfen – und das Sublizenz-Modell ist das Vehikel dafür. Wie ein Reddit-Nutzer es auf den Punkt bringt: „Looks like they're really milking the fat cow as hard as they can. I think the IP goes public domain in 2044, so they're going pretty hard before time runs out." (u/Marksman1977, +3)

+ +

Hinzu kommt eine strukturelle Besonderheit: Asmodee war bereits vor der MEE-Übernahme einer der größten LOTR-Lizenznehmer mit über einem Dutzend Spielen und mehr als 100 Erweiterungen, wie die Embracer-Pressemitteilung vom August 2022 selbst festhält. Nach der Übernahme wurden Rechteinhaber und größter Lizenznehmer eins – die Geburtsstunde des Category-Management-Modells, bei dem Asmodee die Vergabe von Sublizenzen an Dritte koordiniert.

+
+ +
+ +
+

Was die Community sagt

+ +

Der Reddit-Thread auf r/boardgames (73 Upvotes, 36 Kommentare) spiegelt eine gespaltene Community. Die Reaktionen reichen von Begeisterung bis Skepsis – und die interessantesten Stimmen kommen nicht immer von den am höchsten bewerteten Kommentaren.

+ +

Die KI-Kontroverse

+

Der mit Abstand am höchsten bewertete Kommentar (+43) ist eine Warnung: „Info or reminder for those that care: Stoneblade (Ascension) is openly leaning fairly heavily into generative AI now." – u/zoso_coheed. Für viele in der Community ist die Kombination aus KI-generiertem Artwork und Tolkiens Werk ein ethischer Konflikt.

+ +

Sättigungsangst vs. Begeisterung

+

u/wallysmith127 beobachtet: „For the most part it seems like LotR games have been better-than-average (or truly great like Fate of the Fellowship) but saturation and fatigue is a potential risk now." Eine Einschätzung, die nicht unbegründet ist: Seit 2024 erschienen mit Duel for Middle Earth (ein Reskin von 7 Wonders Duel) und Fate of the Fellowship gleich zwei größere LOTR-Spiele aus Asmodee-eigenen Studios. Nun kommen fünf externe Partner hinzu.

+ +

Dem gegenüber steht u/XIIICaesar: „I know some people are tired of the LOTR license, but I feel the LOTR theme is extremely good, I prefer it to all other fantasy themes. We've been treated with games like Fate of the fellowship and Duel for Middle Earth lately; games that marry the theme and game mechanics so well. I can't wait to see what else we'll be getting in the coming years."

+ +

Das Ende des LCG

+

Ein Nebenschauplatz der Diskussion: Das Lord of the Rings: The Card Game (LCG) – einst eines der erfolgreichsten Fantasy Flight-Spiele – ist offiziell out-of-print. u/sennohki merkt an: „I highly doubt they'll do anything specifically for the LCG, considering they've recently stated it's OOP and they have no plans to print more." Ein bedeutender Einschnitt für die Fangemeinde des Spiels.

+ +

Ungewöhnliches Lizenzmodell

+

u/lilbismyfriend300 fragt: „A publisher getting a license then sub-licensing it to other publishers? Is that normal?" Die kurze Antwort: Nein. Dass ein Publisher (Asmodee) die Master-Lizenz hält und Sublizenzen an Wettbewerber vergibt, ist im Brettspielbereich ungewöhnlich – aber bei Mega-IPs wie Mittelerde oder Star Wars wird es zunehmend zum Modell.

+ +

Wunschliste der Community

+

Die Sehnsüchte der Fans werden in den Kommentaren deutlich: „Restoration Games and Unmatched please..." (u/shortielah) und die Idee einer LOTR-Version von Zombicide (u/Physical_Quit_7100: „Instead of zombies, just have orcs, goblins, trolls, corrupted men.") zeigen die Erwartungshaltung: Die Community will Qualität, aber auch Kreativität und thematische Tiefe – nicht bloß Lizenz-Aufgüsse.

+
+ +
+ +
+

Einordnung und Ausblick

+ +

Das Sublizenz-Programm von Asmodee ist strategisch klug. Es verteilt das Produktionsrisiko, erhöht die Marktdurchdringung und maximiert den Ertrag aus der Lizenz vor dem Public-Domain-Stichtag 2044. Gleichzeitig birgt es Risiken:

+ +
    +
  • Sättigung: Fünf zusätzliche Publisher bedeuten fünf zusätzliche Produkt-Roadmaps. Die Gefahr, dass der Markt mit LOTR-Produkten überflutet wird, ist real.
  • +
  • Qualitätskontrolle: Asmodee muss sicherstellen, dass Sublizenznehmer den Qualitätsstandard halten, den die Marke verlangt – insbesondere bei sensiblen Themen wie KI-generiertem Artwork.
  • +
  • Präzedenzfall: Falls das Modell erfolgreich ist, könnten andere IP-Inhaber (Disney für Star Wars, Marvel) ähnliche Sublizenz-Programme aufsetzen – mit weitreichenden Folgen für die gesamte Branche.
  • +
+ +

Ob das Modell aufgeht, wird sich in den kommenden 12 bis 24 Monaten zeigen, wenn die ersten Produkte der Sublizenz-Partner erscheinen. Für die Brettspiel-Community bleibt zu hoffen, dass die Qualität der Spiele mit der strategischen Vision der Lizenzgeber mithalten kann.

+
+ +
+ +
+

Quellen

+
    +
  1. Asmodee Corporate: Offizielle Pressemitteilung (12. Mai 2026) — Primärquelle
  2. +
  3. Embracer Group: Pressemitteilung zum Erwerb von Middle-earth Enterprises (18. August 2022) — Primärquelle (Archiv)
  4. +
  5. Reddit r/boardgames: Diskussions-Thread zur Ankündigung — Community-Reaktionen
  6. +
  7. ICv2: Berichterstattung zur Ankündigung — Branchen-Publikation
  8. +
  9. Wikipedia: Middle-earth Enterprises — Hintergrund zu Geschichte und Übernahme
  10. +
  11. Wikipedia: Embracer Group — Konzernstruktur und Übernahmen
  12. +
  13. Wikipedia: Asmodee — Unternehmensgeschichte, Übernahme durch Embracer, Börsennotierung
  14. +
  15. Wikipedia: J.R.R. Tolkien — Todesdatum (2. September 1973)
  16. +
  17. Wikipedia: Copyright law of the United Kingdom — Schutzfrist Leben + 70 Jahre
  18. +
  19. Wikipedia: Copyright law of the United States — Schutzfrist 95 Jahre ab Veröffentlichung
  20. +
  21. Sirius Dice: Offizielle Website
  22. +
  23. Stone Blade Entertainment: Offizielle Website
  24. +
  25. Game Toppers: Offizielle Website
  26. +
  27. BoardGameGeek: The Lord of the Rings – Duel for Middle Earth
  28. +
  29. Fantasy Flight Games — Asmodee-eigenes Studio, Publisher des LOTR LCG und Journeys in Middle-earth
  30. +
+
+ +

Dieser Artikel wurde am 21. Mai 2026 verfasst. Alle Informationen basieren auf öffentlich zugänglichen Quellen. Die Asmodee-Pressemitteilung vom 12. Mai 2026 wurde für diese aktualisierte Fassung vollständig ausgewertet.

+ +
+ + diff --git a/browser-os/index.html b/browser-os/index.html new file mode 100644 index 0000000..62c433a --- /dev/null +++ b/browser-os/index.html @@ -0,0 +1,496 @@ + + + + + +macOS Browser OS + + + +
+ +
+
⚙️
+
📁
+
💻
+
📝
+
🖩
+
🐍
+
🧱
+
+
+
+ + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..99fb5c7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,115 @@ +version: "3.9" + +services: + postgres: + image: postgres:16 + environment: + POSTGRES_DB: feedbackhub + POSTGRES_USER: ${POSTGRES_USER:-feedback} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-feedback} -d feedbackhub"] + interval: 5s + timeout: 3s + retries: 5 + + redis: + image: redis:7-alpine + command: redis-server --requirepass ${REDIS_PASSWORD:-changeme} --appendonly yes + volumes: + - redisdata:/data + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-changeme}", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin} + volumes: + - miniodata:/data + ports: + - "9000:9000" + - "9001:9001" + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 3s + retries: 5 + + backend: + build: ./backend + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + environment: + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-feedback}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/feedbackhub + REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379/0 + MINIO_ENDPOINT: minio:9000 + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin} + MINIO_BUCKET: feedback-media + SECRET_KEY: ${SECRET_KEY:-dev-secret-change-in-prod} + JWT_ALGORITHM: HS256 + volumes: + - ./backend:/app + - media_processing:/tmp/media + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + minio: + condition: service_healthy + + worker: + build: ./backend + command: python -m app.workers.runner + environment: + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-feedback}:${POSTGRES_PASSWORD:-changeme}@postgres:5432/feedbackhub + REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379/0 + MINIO_ENDPOINT: minio:9000 + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin} + MINIO_BUCKET: feedback-media + SECRET_KEY: ${SECRET_KEY:-dev-secret-change-in-prod} + volumes: + - ./backend:/app + - media_processing:/tmp/media + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + minio: + condition: service_healthy + + frontend: + build: ./frontend + command: npm run dev + environment: + NEXT_PUBLIC_API_URL: http://localhost:8000 + NEXT_PUBLIC_WS_URL: ws://localhost:8000 + volumes: + - ./frontend/src:/app/src + - ./frontend/public:/app/public + ports: + - "3000:3000" + depends_on: + - backend + +volumes: + pgdata: + redisdata: + miniodata: + media_processing: diff --git a/image-folder-watcher/image-folder-watcher/.gitignore b/image-folder-watcher/image-folder-watcher/.gitignore new file mode 100644 index 0000000..dde9041 --- /dev/null +++ b/image-folder-watcher/image-folder-watcher/.gitignore @@ -0,0 +1,5 @@ +.venv/ +.venv-test/ +__pycache__/ +*.pyc +*.log diff --git a/image-folder-watcher/image-folder-watcher/README.md b/image-folder-watcher/image-folder-watcher/README.md new file mode 100644 index 0000000..a577bbb --- /dev/null +++ b/image-folder-watcher/image-folder-watcher/README.md @@ -0,0 +1,83 @@ +# Image Folder Watcher + +Überwacht einen Ordner. Sobald ein **Unterordner mit Bildern** hineingeschoben +wird, werden alle Bilder automatisch: + +1. In einen `Backup/`-Unterordner kopiert (Originale bleiben erhalten) +2. Web-sicher umbenannt: `-00.jpg`, `-01.jpg`, … + (Umlaute → `ae/oe/ue/ss`, Leerzeichen → `-`, alles klein) +3. Auf **1920 px Breite** (proportional) und **72 DPI** skaliert +4. Als JPEG unter **1 MB** gehalten (Qualität wird bei Bedarf abgesenkt) + +Unterstützte Formate: JPEG, PNG, WEBP, TIFF, BMP, GIF, AVIF, HEIC/HEIF. + +Läuft auf **macOS** und **Linux** (gleiche Codebasis). + +## Installation + +```bash +git clone git@git.datenhimmel.work:claude-ai/image-folder-watcher.git +cd image-folder-watcher +./install.sh +``` + +Das Script: +- prüft/installiert Python (Homebrew auf macOS, pacman/apt/dnf/zypper auf Linux), +- legt ein **virtuelles Environment** an (`~/image-folder-watcher/.venv`) und + installiert Pillow, watchdog und pillow-heif darin – **kein** + `--break-system-packages` nötig, +- erstellt den Überwachungsordner + (`~/Pictures/Eingang` auf macOS, `~/Bilder/Eingang` auf Linux), +- bietet optional Autostart an (launchd auf macOS, systemd auf Linux). + +Eigenen Pfad festlegen: + +```bash +WATCH_DIR="$HOME/mein/eingang" ./install.sh +``` + +## Manuell starten + +```bash +~/image-folder-watcher/.venv/bin/python ~/image-folder-watcher/watcher.py "<überwachter-ordner>" +``` + +Mit Optionen: + +```bash +... watcher.py "" --width 1920 --dpi 72 --format JPEG --quality 92 --delay 5 +``` + +| Option | Bedeutung | Standard | +|-------------|---------------------------------------------|----------| +| `--width` | Zielbreite in Pixel | 1920 | +| `--dpi` | Ziel-DPI | 72 | +| `--format` | Ausgabeformat (JPEG/PNG/WEBP) | JPEG | +| `--quality` | JPEG-Qualität (1–100) | 92 | +| `--delay` | Wartezeit bis Ordner als "fertig" gilt (s) | 5 | + +> Bei Netzwerk-/SMB-Kopien (z. B. vom Mac auf einen Linux-Share) `--delay` +> höher setzen, damit das Tool wartet, bis wirklich alle Dateien angekommen sind. + +## Autostart + +**macOS (launchd):** +```bash +launchctl print gui/$(id -u)/work.datenhimmel.image-watcher # Status +launchctl bootout gui/$(id -u)/work.datenhimmel.image-watcher # Stoppen/Entfernen +tail -f ~/image-folder-watcher/watcher.out.log # Logs +``` + +**Linux (systemd):** +```bash +sudo systemctl status image-watcher +journalctl -u image-watcher -f +``` + +## Plattform-Hinweise + +- macOS nutzt FSEvents, Linux nutzt inotify – `watchdog` wählt das automatisch. +- Auf macOS gibt es keine `--break-system-packages`-Problematik, weil alles im + venv landet. +- Der systemd-Dienst läuft als System-Service unter deinem User; der + launchd-Agent läuft als User-Agent (startet beim Login). diff --git a/image-folder-watcher/image-folder-watcher/install.sh b/image-folder-watcher/image-folder-watcher/install.sh new file mode 100755 index 0000000..9d6e8b2 --- /dev/null +++ b/image-folder-watcher/image-folder-watcher/install.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# ══════════════════════════════════════════════════════════════ +# Image Folder Watcher – plattformübergreifende Installation +# Unterstützt: macOS (launchd) und Linux (systemd) +# Nutzt ein Python-venv – kein --break-system-packages nötig. +# ══════════════════════════════════════════════════════════════ + +set -euo pipefail + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' +info() { echo -e "${YELLOW}$*${NC}"; } +ok() { echo -e "${GREEN}$*${NC}"; } +err() { echo -e "${RED}$*${NC}" >&2; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OS="$(uname -s)" + +echo -e "${GREEN}══════════════════════════════════════════════════${NC}" +echo -e "${GREEN} Image Folder Watcher – Installation (${OS})${NC}" +echo -e "${GREEN}══════════════════════════════════════════════════${NC}" + +INSTALL_DIR="${INSTALL_DIR:-$HOME/image-folder-watcher}" +VENV_DIR="$INSTALL_DIR/.venv" + +# Sinnvolle Standard-Überwachungsordner je Plattform (per ENV überschreibbar) +if [[ "$OS" == "Darwin" ]]; then + WATCH_DIR="${WATCH_DIR:-$HOME/Pictures/Eingang}" +else + WATCH_DIR="${WATCH_DIR:-$HOME/Bilder/Eingang}" +fi + +# ── 1. Python sicherstellen + System-Pakete ────────────────────────────────── +info "\n[1/4] Prüfe Python & installiere ggf. System-Voraussetzungen..." + +ensure_python_linux() { + if command -v pacman &>/dev/null; then + sudo pacman -S --needed --noconfirm python + elif command -v apt-get &>/dev/null; then + sudo apt-get update -qq + sudo apt-get install -y python3 python3-venv python3-pip + elif command -v dnf &>/dev/null; then + sudo dnf install -y python3 python3-pip + elif command -v zypper &>/dev/null; then + sudo zypper install -y python3 python3-pip + else + err " Kein bekannter Paketmanager (pacman/apt/dnf/zypper) gefunden." + err " Bitte Python 3 + venv manuell installieren und Script erneut starten." + exit 1 + fi +} + +ensure_python_macos() { + if ! command -v python3 &>/dev/null; then + if command -v brew &>/dev/null; then + brew install python + else + err " Python 3 nicht gefunden und Homebrew ist nicht installiert." + err " Installiere Python von https://www.python.org/downloads/ ODER" + err " Homebrew von https://brew.sh und starte dann erneut." + exit 1 + fi + fi + # Xcode Command Line Tools liefern viele Build-Voraussetzungen; pillow-heif + # nutzt aber vorgebaute Wheels, daher i.d.R. nicht zwingend nötig. +} + +case "$OS" in + Darwin) ensure_python_macos ;; + Linux) ensure_python_linux ;; + *) err " Nicht unterstütztes OS: $OS"; exit 1 ;; +esac + +PYTHON="$(command -v python3)" +ok " ✅ Python: $PYTHON ($($PYTHON --version))" + +# ── 2. venv anlegen & Abhängigkeiten installieren ──────────────────────────── +info "\n[2/4] Erstelle virtuelle Umgebung & installiere Abhängigkeiten..." +mkdir -p "$INSTALL_DIR" +if [[ ! -d "$VENV_DIR" ]]; then + "$PYTHON" -m venv "$VENV_DIR" +fi +"$VENV_DIR/bin/pip" install --upgrade pip --quiet +"$VENV_DIR/bin/pip" install -r "$SCRIPT_DIR/requirements.txt" +ok " ✅ Pillow, watchdog, pillow-heif (HEIC/HEIF) installiert." + +# ── 3. Script + Überwachungsordner ─────────────────────────────────────────── +info "\n[3/4] Kopiere Script nach $INSTALL_DIR & erstelle Ordner..." +cp "$SCRIPT_DIR/watcher.py" "$INSTALL_DIR/watcher.py" +chmod +x "$INSTALL_DIR/watcher.py" +mkdir -p "$WATCH_DIR" +ok " ✅ Überwachungsordner: $WATCH_DIR" + +VENV_PYTHON="$VENV_DIR/bin/python" + +# ── 4. Autostart (optional, plattformabhängig) ─────────────────────────────── +info "\n[4/4] Autostart einrichten?" +read -rp "Autostart-Dienst installieren? [j/N] " INSTALL_SERVICE +if [[ "$INSTALL_SERVICE" =~ ^[jJyY]$ ]]; then + if [[ "$OS" == "Darwin" ]]; then + # ── macOS: launchd User-Agent ── + LABEL="work.datenhimmel.image-watcher" + PLIST_DIR="$HOME/Library/LaunchAgents" + PLIST="$PLIST_DIR/$LABEL.plist" + mkdir -p "$PLIST_DIR" + sed -e "s|__LABEL__|$LABEL|g" \ + -e "s|__PYTHON__|$VENV_PYTHON|g" \ + -e "s|__SCRIPT__|$INSTALL_DIR/watcher.py|g" \ + -e "s|__WATCH_DIR__|$WATCH_DIR|g" \ + -e "s|__WORKDIR__|$INSTALL_DIR|g" \ + "$SCRIPT_DIR/launchd/image-watcher.plist.template" > "$PLIST" + # Neu laden (bootout ignoriert Fehler falls noch nicht geladen) + launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true + launchctl bootstrap "gui/$(id -u)" "$PLIST" + ok " ✅ launchd-Agent installiert & gestartet!" + echo " Status: launchctl print gui/$(id -u)/$LABEL | head" + echo " Stoppen: launchctl bootout gui/$(id -u)/$LABEL" + echo " Logs: tail -f $INSTALL_DIR/watcher.out.log" + else + # ── Linux: systemd System-Service ── + SERVICE_NAME="image-watcher.service" + TMP_SERVICE="$INSTALL_DIR/$SERVICE_NAME" + sed -e "s|__PYTHON__|$VENV_PYTHON|g" \ + -e "s|__SCRIPT__|$INSTALL_DIR/watcher.py|g" \ + -e "s|__WATCH_DIR__|$WATCH_DIR|g" \ + -e "s|__WORKDIR__|$INSTALL_DIR|g" \ + -e "s|__USER__|$USER|g" \ + "$SCRIPT_DIR/systemd/image-watcher.service.template" > "$TMP_SERVICE" + sudo cp "$TMP_SERVICE" "/etc/systemd/system/$SERVICE_NAME" + sudo systemctl daemon-reload + sudo systemctl enable "$SERVICE_NAME" + sudo systemctl restart "$SERVICE_NAME" + ok " ✅ systemd-Service installiert & gestartet!" + echo " Status: sudo systemctl status image-watcher" + echo " Logs: journalctl -u image-watcher -f" + fi +fi + +echo -e "\n${GREEN}══════════════════════════════════════════════════${NC}" +ok " ✅ Installation abgeschlossen!" +echo -e "${GREEN}══════════════════════════════════════════════════${NC}" +echo "" +echo " Manuell starten:" +echo " $VENV_PYTHON $INSTALL_DIR/watcher.py \"$WATCH_DIR\"" +echo "" +echo " Mit Optionen:" +echo " $VENV_PYTHON $INSTALL_DIR/watcher.py \"$WATCH_DIR\" --width 1920 --dpi 72 --format JPEG" +echo "" diff --git a/image-folder-watcher/image-folder-watcher/launchd/image-watcher.plist.template b/image-folder-watcher/image-folder-watcher/launchd/image-watcher.plist.template new file mode 100644 index 0000000..3bf787a --- /dev/null +++ b/image-folder-watcher/image-folder-watcher/launchd/image-watcher.plist.template @@ -0,0 +1,30 @@ + + + + + Label + __LABEL__ + + ProgramArguments + + __PYTHON__ + __SCRIPT__ + __WATCH_DIR__ + + + WorkingDirectory + __WORKDIR__ + + + RunAtLoad + + KeepAlive + + + StandardOutPath + __WORKDIR__/watcher.out.log + StandardErrorPath + __WORKDIR__/watcher.err.log + + diff --git a/image-folder-watcher/image-folder-watcher/requirements.txt b/image-folder-watcher/image-folder-watcher/requirements.txt new file mode 100644 index 0000000..e75c79c --- /dev/null +++ b/image-folder-watcher/image-folder-watcher/requirements.txt @@ -0,0 +1,3 @@ +Pillow>=10.0 +watchdog>=3.0 +pillow-heif>=0.13 diff --git a/image-folder-watcher/image-folder-watcher/systemd/image-watcher.service.template b/image-folder-watcher/image-folder-watcher/systemd/image-watcher.service.template new file mode 100644 index 0000000..0f521ec --- /dev/null +++ b/image-folder-watcher/image-folder-watcher/systemd/image-watcher.service.template @@ -0,0 +1,16 @@ +[Unit] +Description=Image Folder Watcher – Automatische Bildverarbeitung +After=network.target + +[Service] +Type=simple +User=__USER__ +ExecStart=__PYTHON__ __SCRIPT__ __WATCH_DIR__ +WorkingDirectory=__WORKDIR__ +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/image-folder-watcher/image-folder-watcher/watcher.py b/image-folder-watcher/image-folder-watcher/watcher.py new file mode 100644 index 0000000..bafd3bb --- /dev/null +++ b/image-folder-watcher/image-folder-watcher/watcher.py @@ -0,0 +1,465 @@ +#!/usr/bin/env python3 +""" +Image Folder Watcher +==================== +Überwacht einen Zielordner. Wenn ein Unterordner mit Bildern hineingeschoben wird, +werden alle Bilder automatisch: + 1. In einen "Backup"-Unterordner kopiert (Originale erhalten) + 2. Umbenannt nach: _00, _01, ... + 3. Auf 72 DPI und 1920px Breite skaliert (Höhe proportional) + +Unterstützte Formate: JPEG, PNG, WEBP, HEIC, HEIF, TIFF, BMP, GIF, AVIF + +Verwendung: + python watcher.py /pfad/zum/überwachten/ordner + +Abhängigkeiten: + pip install Pillow pillow-heif watchdog + +Für CachyOS/Arch: + sudo pacman -S python-pillow python-watchdog + pip install pillow-heif --break-system-packages (für HEIC/HEIF) +""" + +import sys +import os +import re +import shutil +import time +import logging +import argparse +import unicodedata +from pathlib import Path +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +from PIL import Image + +# HEIC/HEIF-Support optional laden +try: + from pillow_heif import register_heif_opener + register_heif_opener() + HEIF_SUPPORT = True +except ImportError: + HEIF_SUPPORT = False + +# ─── Konfiguration ─────────────────────────────────────────────────────────── + +TARGET_WIDTH = 1920 +TARGET_DPI = 72 +BACKUP_DIR_NAME = "Backup" +OUTPUT_FORMAT = "JPEG" # Ausgabeformat für konvertierte Bilder (JPEG, PNG, WEBP) +JPEG_QUALITY = 92 +MAX_FILE_SIZE = 1_000_000 # Maximale Dateigröße in Bytes (1 MB) +JPEG_QUALITY_MIN = 40 # Untergrenze für JPEG-Qualität bei Größenanpassung + +SUPPORTED_EXTENSIONS = { + ".jpg", ".jpeg", ".png", ".webp", ".tiff", ".tif", + ".bmp", ".gif", ".avif", ".heic", ".heif", +} + +# Stabilisierungszeit: Warten bis keine neuen Dateien mehr kommen (Sekunden) +# Höher setzen bei SMB/Netzwerk-Kopien vom Mac (Standard: 5s) +STABILIZE_DELAY = 5.0 + +# ─── Logging ────────────────────────────────────────────────────────────────── + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s │ %(levelname)-7s │ %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger("watcher") + +# ─── Bildverarbeitung ──────────────────────────────────────────────────────── + + +def sanitize_name(name: str) -> str: + """Macht einen Datei-/Ordnernamen web-sicher. + + - Unicode-Zeichen → ASCII-Äquivalent (ä→ae, ö→oe, ü→ue, ß→ss, é→e, ...) + - Leerzeichen → Bindestrich + - Sonderzeichen entfernen (nur a-z, 0-9, Bindestrich, Unterstrich) + - Mehrfache Bindestriche zusammenfassen + - Alles lowercase + """ + # Deutsche Umlaute explizit ersetzen + replacements = { + "ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss", + "Ä": "Ae", "Ö": "Oe", "Ü": "Ue", + } + for src, dst in replacements.items(): + name = name.replace(src, dst) + + # Restliche Unicode-Zeichen → ASCII (é→e, ñ→n, etc.) + name = unicodedata.normalize("NFKD", name) + name = name.encode("ascii", "ignore").decode("ascii") + + # Leerzeichen und Unterstriche → Bindestrich + name = name.replace(" ", "-").replace("_", "-") + + # Nur erlaubte Zeichen behalten: a-z, 0-9, Bindestrich + name = re.sub(r"[^a-zA-Z0-9\-]", "", name) + + # Mehrfache Bindestriche zusammenfassen + name = re.sub(r"-{2,}", "-", name) + + # Führende/trailing Bindestriche entfernen + name = name.strip("-") + + # Lowercase + name = name.lower() + + return name if name else "image" + + +def get_image_files(folder: Path) -> list[Path]: + """Sammelt alle unterstützten Bilddateien rekursiv (inkl. Unterordner). + Ignoriert den Backup-Ordner.""" + files = [] + for f in sorted(folder.rglob("*")): + if f.is_file() and f.suffix.lower() in SUPPORTED_EXTENSIONS: + # Backup-Ordner überspringen + if BACKUP_DIR_NAME not in f.relative_to(folder).parts: + files.append(f) + return files + + +def fix_permissions(folder: Path) -> None: + """Versucht Schreibrechte rekursiv zu setzen. + Auf SMB/NFS-Mounts schlägt chmod oft fehl – dann einfach weitermachen, + da die Berechtigungen dort vom Mount selbst gesteuert werden.""" + try: + folder.chmod(0o755) + for item in folder.rglob("*"): + if item.is_file(): + item.chmod(0o644) + elif item.is_dir() and item.name != BACKUP_DIR_NAME: + item.chmod(0o755) + log.info(f" 🔓 Berechtigungen rekursiv korrigiert") + except (PermissionError, OSError): + log.info(f" ℹ️ chmod nicht möglich (Netzwerk-Mount?) – fahre trotzdem fort") + + +def process_folder(folder: Path) -> None: + """Verarbeitet einen neu erkannten Ordner mit Bildern.""" + folder_name = folder.name + log.info(f"📂 Verarbeite Ordner: {folder_name}") + + # ── 0. Berechtigungen korrigieren (Mac → Linux) ── + fix_permissions(folder) + + images = get_image_files(folder) + if not images: + log.warning(f" Keine unterstützten Bilder in '{folder_name}' gefunden. Überspringe.") + return + + log.info(f" {len(images)} Bild(er) gefunden.") + + # ── 1. Backup-Ordner erstellen und Originale sichern (Struktur erhalten) ── + backup_dir = folder / BACKUP_DIR_NAME + backup_dir.mkdir(exist_ok=True) + log.info(f" 💾 Backup-Ordner: {backup_dir}") + + for img_path in images: + # Relativen Pfad beibehalten für Unterordner-Struktur im Backup + rel_path = img_path.relative_to(folder) + backup_path = backup_dir / rel_path + backup_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(img_path, backup_path) + log.info(f" ↳ Backup: {rel_path}") + + # ── 2. Umbenennen und alle Bilder ins Hauptverzeichnis verschieben ── + # Ordnername web-sicher machen + safe_name = sanitize_name(folder_name) + log.info(f" 🌐 Web-sicherer Name: '{folder_name}' → '{safe_name}'") + + # Bestimme Ausgabe-Endung + ext_map = { + "JPEG": ".jpg", + "PNG": ".png", + "WEBP": ".webp", + } + out_ext = ext_map.get(OUTPUT_FORMAT, ".jpg") + + # Erst alle in temporäre Namen verschieben (vermeidet Kollisionen) + temp_files: list[tuple[Path, Path]] = [] + for idx, img_path in enumerate(images): + temp_name = f".tmp_{idx:04d}{img_path.suffix}" + temp_path = folder / temp_name + shutil.move(str(img_path), str(temp_path)) + temp_files.append((temp_path, folder / f"{safe_name}-{idx:02d}{out_ext}")) + + # Dann in finale Namen umbenennen + for temp_path, final_path in temp_files: + if final_path.exists(): + final_path.unlink() + temp_path.rename(final_path) + log.info(f" ✏️ Umbenannt → {final_path.name}") + + # Leere Unterordner aufräumen (außer Backup) + for sub in sorted(folder.iterdir(), reverse=True): + if sub.is_dir() and sub.name != BACKUP_DIR_NAME: + try: + sub.rmdir() # Nur wenn leer + log.info(f" 🗑️ Leerer Unterordner entfernt: {sub.name}") + except OSError: + pass # Nicht leer, ignorieren + + # ── 3. Skalieren auf 1920px Breite, 72 DPI ── + final_files = get_image_files(folder) + for img_path in final_files: + try: + resize_image(img_path) + except Exception as e: + log.error(f" ❌ Fehler bei '{img_path.name}': {e}") + + log.info(f"✅ Ordner '{folder_name}' fertig verarbeitet!\n") + + +def resize_image(img_path: Path) -> None: + """Skaliert ein Bild auf TARGET_WIDTH px Breite (proportional), setzt 72 DPI, + und stellt sicher dass die Dateigröße unter MAX_FILE_SIZE bleibt.""" + with Image.open(img_path) as img: + # EXIF-Rotation anwenden (wichtig bei Handy-Fotos!) + from PIL import ImageOps + img = ImageOps.exif_transpose(img) + + orig_w, orig_h = img.size + + if orig_w == 0: + log.warning(f" ⚠️ Überspringe '{img_path.name}' (Breite = 0)") + return + + # Neues Seitenverhältnis berechnen + ratio = TARGET_WIDTH / orig_w + new_h = round(orig_h * ratio) + + img_resized = img.resize((TARGET_WIDTH, new_h), Image.LANCZOS) + + # In RGB konvertieren falls nötig (z.B. RGBA → RGB für JPEG) + if OUTPUT_FORMAT == "JPEG" and img_resized.mode in ("RGBA", "P", "LA"): + background = Image.new("RGB", img_resized.size, (255, 255, 255)) + if img_resized.mode == "P": + img_resized = img_resized.convert("RGBA") + background.paste(img_resized, mask=img_resized.split()[-1] if "A" in img_resized.mode else None) + img_resized = background + + # Speichern mit DPI – bei JPEG iterativ Qualität senken bis < MAX_FILE_SIZE + save_kwargs = {"dpi": (TARGET_DPI, TARGET_DPI)} + + if OUTPUT_FORMAT == "JPEG": + quality = JPEG_QUALITY + save_kwargs["optimize"] = True + + while quality >= JPEG_QUALITY_MIN: + save_kwargs["quality"] = quality + img_resized.save(img_path, format=OUTPUT_FORMAT, **save_kwargs) + file_size = img_path.stat().st_size + + if file_size <= MAX_FILE_SIZE: + size_kb = file_size / 1024 + log.info( + f" 📐 {img_path.name}: {orig_w}×{orig_h} → {TARGET_WIDTH}×{new_h} " + f"@ {TARGET_DPI} DPI, Q{quality}, {size_kb:.0f} KB" + ) + return + + # Qualität reduzieren und nochmal versuchen + quality -= 5 + log.debug(f" {img_path.name}: {file_size/1024:.0f} KB > {MAX_FILE_SIZE/1024:.0f} KB – senke Qualität auf {quality}") + + # Untergrenze erreicht, trotzdem speichern + file_size = img_path.stat().st_size + size_kb = file_size / 1024 + log.warning( + f" ⚠️ {img_path.name}: {size_kb:.0f} KB – konnte nicht unter " + f"{MAX_FILE_SIZE/1024:.0f} KB gebracht werden (Q{JPEG_QUALITY_MIN})" + ) + else: + img_resized.save(img_path, format=OUTPUT_FORMAT, **save_kwargs) + file_size = img_path.stat().st_size + size_kb = file_size / 1024 + log.info( + f" 📐 {img_path.name}: {orig_w}×{orig_h} → {TARGET_WIDTH}×{new_h} " + f"@ {TARGET_DPI} DPI, {size_kb:.0f} KB" + ) + + +# ─── Ordner-Überwachung ────────────────────────────────────────────────────── + + +class FolderHandler(FileSystemEventHandler): + """Erkennt neue Unterordner und verarbeitet sie nach Stabilisierung. + Filtert temporäre Ordner (macOS SMB: .::TMPNAME:..., .DS_Store, ._*, etc.)""" + + # Muster für temporäre/versteckte Ordner die ignoriert werden sollen + IGNORE_PREFIXES = (".", ".::TMPNAME", "._", "__MACOSX") + + def __init__(self): + super().__init__() + self._pending: dict[str, float] = {} + self._processed: set[str] = set() + + def _should_ignore(self, dir_path: Path) -> bool: + """Prüft ob ein Ordner ignoriert werden soll.""" + name = dir_path.name + if name == BACKUP_DIR_NAME: + return True + for prefix in self.IGNORE_PREFIXES: + if name.startswith(prefix): + return True + return False + + def on_created(self, event): + if event.is_directory: + dir_path = Path(event.src_path) + if self._should_ignore(dir_path): + log.debug(f" Ignoriere temporären Ordner: {dir_path.name}") + return + if str(dir_path) not in self._processed: + log.info(f"🔍 Neuer Ordner erkannt: {dir_path.name} – warte auf Stabilisierung...") + self._pending[str(dir_path)] = time.time() + + def on_moved(self, event): + """Fängt Umbenennungen ab – macOS SMB erstellt .::TMPNAME:... und benennt dann um.""" + if event.is_directory: + dest_path = Path(event.dest_path) + src_path = Path(event.src_path) + + # Temporären Eintrag entfernen falls vorhanden + src_str = str(src_path) + if src_str in self._pending: + del self._pending[src_str] + + if self._should_ignore(dest_path): + return + + if str(dest_path) not in self._processed: + log.info(f"🔍 Ordner umbenannt: {src_path.name} → {dest_path.name} – warte auf Stabilisierung...") + self._pending[str(dest_path)] = time.time() + + def on_modified(self, event): + """Auch Modifikationen tracken (Dateien die in bestehenden Ordner kommen).""" + if event.is_directory: + dir_path = Path(event.src_path) + dir_str = str(dir_path) + if self._should_ignore(dir_path): + return + if dir_str in self._pending: + self._pending[dir_str] = time.time() + + def check_pending(self): + """Prüft ob ausstehende Ordner stabil sind (keine neuen Dateien seit STABILIZE_DELAY).""" + now = time.time() + ready = [] + for dir_str, last_change in list(self._pending.items()): + if now - last_change >= STABILIZE_DELAY: + ready.append(dir_str) + + for dir_str in ready: + del self._pending[dir_str] + folder = Path(dir_str) + # Nochmal prüfen: existiert der Ordner noch und ist er kein Temp-Ordner? + if folder.exists() and folder.is_dir() and not self._should_ignore(folder): + self._processed.add(dir_str) + try: + process_folder(folder) + except Exception as e: + log.error(f"❌ Fehler bei Verarbeitung von '{folder.name}': {e}") + + +# ─── Main ───────────────────────────────────────────────────────────────────── + + +def main(): + parser = argparse.ArgumentParser( + description="Überwacht einen Ordner und verarbeitet neue Bild-Unterordner automatisch.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Beispiel: + python watcher.py ~/Bilder/Eingang + +Was passiert: + 1. Du schiebst einen Ordner (z.B. "Produkt-Fotos") in ~/Bilder/Eingang + 2. Das Tool wartet kurz bis alle Dateien da sind + 3. Originale werden nach Produkt-Fotos/Backup/ kopiert + 4. Bilder werden umbenannt zu Produkt-Fotos_00.jpg, Produkt-Fotos_01.jpg, ... + 5. Bilder werden auf 1920px Breite / 72 DPI skaliert + """, + ) + parser.add_argument( + "watch_dir", + type=str, + help="Pfad zum Ordner der überwacht werden soll", + ) + parser.add_argument( + "--width", type=int, default=TARGET_WIDTH, + help=f"Zielbreite in Pixel (Standard: {TARGET_WIDTH})", + ) + parser.add_argument( + "--dpi", type=int, default=TARGET_DPI, + help=f"Ziel-DPI (Standard: {TARGET_DPI})", + ) + parser.add_argument( + "--format", type=str, default=OUTPUT_FORMAT, + choices=["JPEG", "PNG", "WEBP"], + help=f"Ausgabeformat (Standard: {OUTPUT_FORMAT})", + ) + parser.add_argument( + "--quality", type=int, default=JPEG_QUALITY, + help=f"JPEG-Qualität 1-100 (Standard: {JPEG_QUALITY})", + ) + parser.add_argument( + "--delay", type=float, default=STABILIZE_DELAY, + help=f"Wartezeit in Sekunden bis Ordner als stabil gilt (Standard: {STABILIZE_DELAY})", + ) + + args = parser.parse_args() + + # Konfiguration aus CLI-Argumenten in Modul-Variablen schreiben + # WICHTIG: Die laufende Modul-Instanz referenzieren (beim direkten Start + # ist das "__main__", nicht das per Name importierte "watcher"). Sonst + # landen die CLI-Overrides im falschen Modul und greifen nicht. + _self = sys.modules[__name__] + _self.TARGET_WIDTH = args.width + _self.TARGET_DPI = args.dpi + _self.OUTPUT_FORMAT = args.format + _self.JPEG_QUALITY = args.quality + _self.STABILIZE_DELAY = args.delay + + watch_path = Path(args.watch_dir).expanduser().resolve() + if not watch_path.exists(): + log.info(f"📁 Erstelle Überwachungsordner: {watch_path}") + watch_path.mkdir(parents=True, exist_ok=True) + + if not HEIF_SUPPORT: + log.warning("⚠️ pillow-heif nicht installiert – HEIC/HEIF-Dateien werden nicht unterstützt.") + log.warning(" Installiere mit: pip install pillow-heif --break-system-packages") + + log.info(f"{'═' * 60}") + log.info(f" 🖼️ Image Folder Watcher gestartet") + log.info(f" 📂 Überwache: {watch_path}") + log.info(f" 📐 Ziel: {TARGET_WIDTH}px Breite, {TARGET_DPI} DPI, {OUTPUT_FORMAT}") + log.info(f" ⏱️ Stabilisierung: {STABILIZE_DELAY}s") + log.info(f"{'═' * 60}") + log.info(f" Warte auf neue Ordner... (Strg+C zum Beenden)\n") + + handler = FolderHandler() + observer = Observer() + observer.schedule(handler, str(watch_path), recursive=False) + observer.start() + + try: + while True: + handler.check_pending() + time.sleep(0.5) + except KeyboardInterrupt: + log.info("\n👋 Watcher beendet.") + observer.stop() + + observer.join() + + +if __name__ == "__main__": + main()