Metadata-Version: 2.4
Name: libresignage
Version: 0.7.0
Summary: An open-source digital signage web application.
License-Expression: MIT
License-File: LICENSE
Keywords: digital-signage,flask,signage
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Flask
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.10
Requires-Dist: flask<4,>=3.1
Requires-Dist: python-dotenv<2,>=1.0
Requires-Dist: segno<2,>=1.6
Provides-Extra: web-snapshots
Requires-Dist: playwright<2,>=1.50; extra == 'web-snapshots'
Description-Content-Type: text/markdown

# LibreSignage

LibreSignage is an open-source digital signage web application built with
Flask. The project uses Flask's application factory pattern and blueprints so
features can be developed and tested independently.

## Requirements

- Python 3.10 or newer
- Node.js 20 or newer
- [uv](https://docs.astral.sh/uv/) (recommended) or another Python package
  installer

## Development

Clone the repository, install the locked development environment, and start
Flask:

```console
uv sync --dev
npm install
npm run build
uv run flask --app libresignage run --debug
```

Then open <http://127.0.0.1:5000>.

The content library at <http://127.0.0.1:5000/content/> accepts images, video,
PDFs, text, QR codes, clocks, web pages, and RSS or Atom feed URLs. Each item
has a default display duration. QR codes can encode a validated website or a
vCard assembled from contact name, organization, phone, email, and website
fields plus an optional structured street, city, state/region, postal code,
and country address. LibreSignage generates high-error-correction SVG codes
locally without a third-party QR service, and caches them for offline players.
Clock widgets
use an IANA timezone and continue updating without a network connection. Feed
widgets render safe, readable headline cards, refresh every five minutes, and
retain the last successful response when the upstream provider fails. Feed
downloads reject redirects, oversized responses, private-network targets,
invalid XML, and unsafe link schemes. Web-page widgets remain available for
sites that permit iframe embedding.

Use <http://127.0.0.1:5000/content/playlists> to arrange reusable content into
ordered playlists, override an entry's duration, choose subtle fade, slide, or
soft-zoom transitions, and
configure continuous looping or one-time playback. Players skip failed media
and continue through the sequence. Metadata and cached feed responses are
stored in `src/instance/libresignage.sqlite` and uploaded files in
`src/instance/uploads/` by default. Deployments can set the `DATABASE`,
`UPLOAD_FOLDER`, and `MAX_CONTENT_LENGTH` Flask configuration values to change
those locations and the upload-size limit.

Uploaded videos are automatically normalized to muted WebM/VP9 so the same
asset plays in browser and Linux Qt screen clients. Install `ffmpeg` on the
LibreSignage server before accepting video uploads. Conversion runs in a
background worker: the library shows its status, refreshes while work is in
progress, and excludes unfinished videos from playback. Conversion behavior,
the FFmpeg executable, and the 30-minute conversion timeout can be changed with
`LIBRESIGNAGE_VIDEO_TRANSCODING_ENABLED`, `LIBRESIGNAGE_FFMPEG_BINARY`,
`LIBRESIGNAGE_FFPROBE_BINARY`, and `LIBRESIGNAGE_VIDEO_TRANSCODE_TIMEOUT`.

The library supports case-insensitive search across titles, text or URLs,
folders, and tags, plus exact folder filtering. Assets can have a folder, up
to ten normalized tags, and an optional expiry date. Expired assets remain
visible for auditing and cleanup but are automatically skipped during
playlist, scheduled, layout, preview, and managed-screen playback. Usage
badges identify every referencing playlist; deleting an in-use asset requires
explicit confirmation. **Edit / replace** updates metadata or replaces the
underlying file, text, URL, feed, QR payload, or clock timezone without changing
the asset ID or its playlist positions. SHA-256 checksums reject duplicate
uploaded files, and successful replacement removes the superseded managed
file.

Use <http://127.0.0.1:5000/content/schedules> to assign playlists to date
ranges, recurring weekdays, and local-time dayparts. Overnight windows are
supported, and rules can target all screens or one screen group. Whole-date
blackouts and playlist overrides handle holidays and one-off programming, with
Bootstrap month and week calendar views for operators. The scheduled player
checks for selection changes every 30 seconds while
continuing its current playback if the server is temporarily unavailable.

The centralized CMS at <http://127.0.0.1:5000/dashboard/> summarizes content,
playlists, schedules, and registered screens. Each screen receives a permanent
player URL and can use either the scheduled channel or a directly assigned
playlist. Assignment changes remain staged until an operator publishes them.
Connected players check for new published revisions every two seconds and
atomically reload the new content. Failed checks back off to 15 seconds while
players continue the last available revision. Every publish is recorded as a
fleet batch on the dashboard.

Administrators can activate a full-screen emergency override from the CMS
dashboard for selected screens, a reusable screen group, or the entire fleet.
Critical, warning, and information styles use large high-contrast messaging
and interrupt playback within the normal two-second player polling interval.
The override is cached as the screen's current offline revision. The explicit
**Restore published playback** action clears it without changing the screen's
playlist, schedule, or multi-zone assignment.

Every managed screen has a **Preview draft** action on the CMS dashboard.
Preview renders the staged playlist, currently active scheduled channel, or
multi-zone layout with the same templates used by the player, without
publishing, advancing its revision, or recording device contact. Operators can
inspect Full HD landscape, Full HD portrait, 4K landscape, and HD landscape
canvases; each uses its actual logical resolution and scales to fit the browser.
External web content may block iframe previews through its own security policy.

Managed players also prepare the complete published playlist for offline use
with a per-screen service worker. Local images, video, PDFs, text, and player
assets are stored in revisioned browser caches with no application expiry;
failed cache updates retain the last complete revision. Cached video supports
byte-range playback. RSS feeds include their last successful publish-time
snapshot. Web items can opt into a static publish-time screenshot when the
optional Playwright capture service is enabled; live web items without one
retain the explicit offline message. The player checks the revision estimate
against available browser storage before caching and reports capacity failures
to the dashboard. Browser storage quotas and device-level cache eviction still
apply. See
[Reboot-safe offline player](docs/offline-player.md) for provisioning,
cold-start verification, and kiosk browser-profile requirements.

LibreSignage includes a standalone PyWebView screen client for native kiosk
deployment. It stores an end-user-provided server base URL and registered
screen slug, runs the resulting managed-player URL fullscreen, and uses a
persistent browser profile so service workers and offline revisions survive
client and system restarts. See
[PyWebView screen client](client/README.md) for installation, configuration,
diagnostics, renderer selection, and graphical-session autostart.

Managed players report a health heartbeat every 5 seconds with their cache
revision, service-worker state, browser storage usage, network state, last
played item, and last cache error. The dashboard classifies screens as online, degraded, offline, or
never connected and opens persistent offline alerts after
`LIBRESIGNAGE_DEVICE_OFFLINE_AFTER_SECONDS` (90 seconds by default). Operators
can remotely restart the browser player, request **Sync now**, or clear
its LibreSignage caches.

Managed players also retain client-reported proof-of-play events in IndexedDB
while offline and upload them with authenticated heartbeats after reconnecting.
The **Proof of play** report filters by date, screen, asset, zone, and outcome
and exports the same result as CSV. A natural duration completion is recorded
as played, an interrupted attempt as skipped, and a detectable media failure as
an error. This is playback lifecycle evidence rather than pixel or audience
verification. History is retained for 90 days by default; configure
`LIBRESIGNAGE_PLAYBACK_LOG_RETENTION_DAYS` to use another positive number of
days.

Run `uv run libresignage-monitor` beside the web server to deliver durable
screen-health transitions, recovery, 15-minute fleet digests, hourly reminders,
publish failures, video-transcode failures, and emergency activations through
configured SMTP, HMAC-signed webhook, and Telegram destinations. `--once`
performs one cycle for diagnostics, cron, or a systemd timer. A supervised
long-running worker should use the same environment, working directory, user,
and database as the web app. Delivery attempts are stored in SQLite and retried
with bounded exponential backoff, so restarting the worker does not lose alerts.

Alert destinations and credentials use the `LIBRESIGNAGE_ALERT_*` variables in
`.env.example`. SMTP supports `starttls`, `tls`, and `none`; webhook URLs must
resolve exclusively to public addresses and require a shared signing secret;
Telegram requires a bot token and comma-separated chat IDs. Webhook requests
sign the exact JSON body in `X-LibreSignage-Signature` as
`sha256=<hex HMAC-SHA256>`, and expose the event name in
`X-LibreSignage-Event`. Run the following after changing alert configuration:

```console
uv run libresignage-monitor --once
```

The native client can opt into independent OS command polling with
`configure --allow-os-reboot`. The dashboard shows the reboot action only while
that agent is connected. Linux uses `systemctl reboot` and Windows uses
`shutdown.exe /r /t 5`; the local client account must have reboot permission.

To enable optional web screenshots, install the extra and its Chromium binary,
then set `LIBRESIGNAGE_WEB_SNAPSHOTS_ENABLED=true`:

```console
uv sync --extra web-snapshots
uv run playwright install chromium
```

The screen inventory at <http://127.0.0.1:5000/dashboard/screens> stores each
display's stable identity, location, operator notes, and reusable group
membership. Operators can stage a scheduled channel or direct playlist for
explicitly selected screens, one group, or the complete fleet. Bulk targeting
resolves to concrete screen IDs when staged; the existing publish action then
delivers those assignments to players.

Each inventory record also stores a logical display profile: width, height,
derived landscape or portrait orientation, and media scaling. Common Full HD,
4K, and HD presets are available alongside custom dimensions from 320 to 7,680
pixels. **Fit with letterboxing** preserves the whole image, **Fill and crop**
covers the display, and **Stretch to fill** matches its bounds. Managed
playback exposes this metadata, applies the chosen image/video scaling, and
rearranges split multi-zone layouts vertically for portrait screens. Draft
preview starts with the saved profile and can temporarily compare other
resolutions and scaling modes.

Multi-zone layouts at <http://127.0.0.1:5000/content/layouts> use fixed,
predictable templates: 70/30 split, main with ticker, and 70/30 split with
ticker. Each zone owns an independent playlist, duration loop, transition, and
failure-recovery cycle. Layouts can be staged to individual screens, groups, or
the full fleet and their complete local media set is included in offline
caching.

Accessible content templates at <http://127.0.0.1:5000/content/templates>
provide announcement, promotion, and information-notice starting points.
Operators substitute labeled text fields, brand and text colors, and an
optional image—no design software is required. LibreSignage enforces WCAG AA
4.5:1 text contrast, requires descriptive alternative text for uploaded
template images, and safely limits text lengths. Created designs become normal
library items, so they can be reused in playlists, schedules, and multi-zone
layouts and are included in managed-player offline caches. Restaurant-focused
presets add a structured six-item menu board, daily special, combo deal, and
food-truck stop announcement with validated prices and menu rows.

The CMS requires an individual user account. On a new installation, visit
<http://127.0.0.1:5000/auth/setup> once to create the first administrator.
Administrators manage accounts at <http://127.0.0.1:5000/auth/users>.
Passwords must contain at least 12 characters and are stored as salted hashes.
The built-in roles are:

- **Admin:** manage users, devices, groups, remote commands, and publishing.
- **Editor:** create and update content, playlists, layouts, schedules, and
  staged screen assignments, but cannot publish or administer users/devices.
- **Viewer:** inspect the CMS without making changes.

Managed player URLs, media delivery, offline assets, and status checks remain
available without an interactive login so kiosk devices can recover after a
reboot. Health heartbeats and remote-command delivery require the per-screen
bearer token shown to administrators in the screen inventory. Existing native
clients must be reconfigured with that token after upgrading. Other CMS routes
require authentication, state-changing browser requests use CSRF protection,
and failed logins are throttled by username and client address. Production
startup fails unless `LIBRESIGNAGE_SECRET_KEY` is explicitly configured.

Configuration profiles live in `src/libresignage/config.py`. LibreSignage uses
the `development` profile by default; set `LIBRESIGNAGE_CONFIG` to
`development`, `testing`, or `production`, or pass `config_name` to the
factory:

```python
app = create_app(config_name="production")
```

The shared configuration also reads `LIBRESIGNAGE_SECRET_KEY`,
`LIBRESIGNAGE_DATABASE`, `LIBRESIGNAGE_UPLOAD_FOLDER`, and
`LIBRESIGNAGE_MAX_CONTENT_LENGTH` from the environment.
Template-bound Google Sheets rows use a last-known-good cache. Configure its
refresh interval, HTTP timeout, and maximum downloaded CSV size with
`LIBRESIGNAGE_TEMPLATE_DATA_REFRESH_SECONDS`,
`LIBRESIGNAGE_TEMPLATE_DATA_TIMEOUT_SECONDS`, and
`LIBRESIGNAGE_TEMPLATE_DATA_MAX_BYTES`.
`LIBRESIGNAGE_TIMEZONE` sets the default IANA timezone shown when creating a
schedule. `LIBRESIGNAGE_DEVICE_OFFLINE_AFTER_SECONDS` controls the device
heartbeat timeout. `LIBRESIGNAGE_TRUSTED_PROXY_COUNT` tells login throttling how
many controlled reverse-proxy hops to trust when resolving the client address;
leave it at zero for direct connections.
Copy `.env.example` to `.env` for local development, replace the sample
secret, and start Flask from the project root. LibreSignage automatically loads
that file; variables already exported by the operating system take precedence.

```console
cp .env.example .env
```

### Local CI verification

Before pushing, run the same locked dependency, test, lint, and package checks
used by GitHub Actions with one command:

```console
./scripts/check-ci.sh
```

The script stops at the first failure and builds packages in a temporary
directory that it removes automatically. To run or troubleshoot individual
checks:

```console
uv lock --check
uv sync --locked --dev
uv run --locked pytest
uv run --locked ruff check .
uv build --out-dir /tmp/libresignage-dist
uv run --locked twine check /tmp/libresignage-dist/*
```

If `uv lock --check` reports that the lockfile needs to be updated after a
dependency or project-version change, run `uv lock`, review and commit the
resulting `uv.lock` change, then repeat the verification commands.

Frontend styles live in `src/libresignage/static/scss/app.scss`. It imports
Bootstrap's Sass source so Bootstrap variables can be overridden before the
import. Run `npm run watch:css` while editing styles, or `npm run build` to
compile the production CSS and copy Bootstrap's bundled JavaScript.

The package also exposes `libresignage:create_app` for WSGI servers:

```python
from libresignage import create_app

app = create_app()
```

## Building and publishing

Build both distribution formats with `uv build`. GitHub Actions validates every
push to a supported working branch and every pull request into `main`. Work in
short-lived `feature/*` or `fix/*` branches and merge them through a pull
request. See [CONTRIBUTING.md](CONTRIBUTING.md) for the branch and release
workflow.

Publishing is deliberately release-driven so ordinary pushes can never upload
an immutable package version. After the version change has passed CI and been
merged to `main`, publish a GitHub Release whose tag is exactly `v` followed by
the version in `pyproject.toml` (for example, `v0.5.0`). The **Publish to PyPI**
workflow verifies that the tagged commit belongs to `main`, builds and checks
the distributions, and uploads them using PyPI Trusted Publishing through the
protected `pypi` GitHub environment.

## License

LibreSignage is distributed under the MIT License. See [LICENSE](LICENSE).
