# Web Link Playlist Items — Player Integration Guide

This document describes the changes required on the **Kiwy-Signage player**
(<https://gitea.moto-adv.com/ske087/Kiwy-Signage.git>) to support a new
playlist item type: **`weblink`** (display a live web page / URL instead of an
uploaded media file).

> The DigiServer (this repo, `digiserver-v2`) side will be updated to emit
> `weblink` items in the playlist API. The player does **not** yet support them.
> Use this guide to implement the player side later.

---

## 1. Background — how items flow today

```
DigiServer API  ──JSON──▶  player sync (get_playlists_v2.py)  ──▶  playlist.json  ──▶  main.py renders
   /api/playlists           downloads files to media/                                  by file extension
```

Each playlist item the server returns currently looks like:

```json
{
  "id": 42,
  "file_name": "promo.jpg",
  "type": "image",
  "duration": 10,
  "position": 1,
  "url": "https://server/digiserver/static/uploads/promo.jpg",
  "edit_on_player": false
}
```

The player:
1. **Syncs** (`src/get_playlists_v2.py` → `download_media_files()`): downloads
   `url` into the local `media/` directory, then rewrites each item keeping only
   `file_name`, `url` (now a **local relative path**), `duration`,
   `edit_on_player`. **Note: the `type` field is currently discarded here.**
2. **Renders** (`src/main.py` → `play_current_media()`): opens the local file
   and chooses a Kivy widget purely by **file extension**
   (`.mp4/.avi/...` → `Video`, `.jpg/.png/...` → `AsyncImage`). Unknown
   extensions are skipped as "unsupported".

A web link breaks all three assumptions: there is no file to download, no
extension to switch on, and no widget that renders a web page.

---

## 2. New server contract (what DigiServer will send)

A web-link playlist item will look like this:

```json
{
  "id": 91,
  "file_name": "weblink-3f9c1a2b",
  "type": "weblink",
  "duration": 30,
  "position": 4,
  "url": "https://example.com/dashboard",
  "edit_on_player": false
}
```

Key differences vs. a file item:

| Field        | File item                         | Web-link item                          |
|--------------|-----------------------------------|----------------------------------------|
| `type`       | `image` / `video`                 | **`weblink`**                          |
| `url`        | Path to a file on the server      | **The web page to display (the link)** |
| `file_name`  | Real filename on disk             | Synthetic id (`weblink-<uuid>`), **no file exists** |

The player must branch on `type == "weblink"` and treat `url` as the page to
open — **never** try to download it as a file.

---

## 3. Required player changes

### 3.1 Sync step — `src/get_playlists_v2.py`

**Function:** `download_media_files(playlist, media_dir, ...)`

1. **Skip download for web links.** At the top of the per-item loop, detect
   `media.get('type') == 'weblink'` and do **not** call `session.get()` / write
   any file for it.
2. **Preserve `type` and the original `url`.** The `updated_media` dict that is
   appended to `updated_playlist` currently drops `type` and rewrites `url` to a
   local path. It must now carry `type` through, and for web links keep `url`
   as the original web address (do not convert to a local relative path).

Suggested shape of the per-item logic:

```python
item_type = media.get('type', '')

if item_type == 'weblink':
    # No file to download — pass the web link through unchanged.
    updated_playlist.append({
        'file_name': media.get('file_name', ''),
        'type': 'weblink',
        'url': media.get('url', ''),          # the actual web page
        'duration': media.get('duration', 10),
        'edit_on_player': False,
    })
    continue

# ... existing download logic for file items ...
updated_playlist.append({
    'file_name': file_name,
    'type': item_type,                        # <-- now preserved
    'url': os.path.relpath(local_path, os.path.dirname(media_dir)),
    'duration': duration,
    'edit_on_player': media.get('edit_on_player', False),
})
```

3. **`delete_unused_media()`** walks `media/` using `file_name`. Web links have
   no file, so they simply won't match anything on disk — no change strictly
   required, but make sure a missing local file for a `weblink` item does not
   trigger a re-download or an error elsewhere.

### 3.2 Render step — `src/main.py`

**Function:** `play_current_media(self, force_reload=False)`

The current logic builds `media_path = os.path.join(self.media_dir, file_name)`
and then does `os.stat(media_path)` — which will fail for a web link (no file).
Add a **web-link branch before the file-existence check**:

```python
media_item = self.playlist[self.current_index]
file_name = media_item.get('file_name', '')
duration = media_item.get('duration', 10)

# NEW: handle web links before any file/path handling
if media_item.get('type') == 'weblink':
    self.play_weblink(media_item.get('url', ''), duration)
    return

# ... existing file existence check + extension branching ...
```

Then add a new method `play_weblink(self, url, duration)`:

- Validate the scheme is `http`/`https` (reject anything else, e.g. `file://`).
- Open the page for `duration` seconds, then advance with `self.next_media()`.
- Wrap in `try/except`; on failure increment `self.consecutive_errors` and call
  `self.next_media()`, matching the existing error-handling pattern.
- Make sure the previous widget (`self.current_widget`) is removed/stopped just
  like the image/video paths do.

#### Rendering approach (pick one)

Kivy has **no production-grade embedded web view**, especially on Raspberry Pi.
Recommended options, in order of robustness:

1. **Chromium kiosk overlay (recommended).** Launch Chromium over the Kivy
   window for the item's duration, then close it and return to Kivy:

   ```python
   import subprocess, shutil
   from urllib.parse import urlparse
   from kivy.clock import Clock

   def play_weblink(self, url, duration):
       scheme = urlparse(url).scheme.lower()
       if scheme not in ('http', 'https'):
           Logger.warning(f"SignagePlayer: Refusing non-http(s) weblink: {url}")
           self.next_media()
           return
       try:
           browser = shutil.which('chromium-browser') or shutil.which('chromium')
           self._weblink_proc = subprocess.Popen([
               browser,
               '--kiosk', '--app=' + url,
               '--noerrdialogs', '--disable-infobars',
               '--incognito', '--no-first-run',
               '--check-for-update-interval=31536000',
           ])
           Clock.schedule_once(lambda dt: self._close_weblink_and_next(), duration)
       except Exception as e:
           Logger.error(f"SignagePlayer: Error opening weblink: {e}")
           self.consecutive_errors += 1
           self.next_media()

   def _close_weblink_and_next(self):
       proc = getattr(self, '_weblink_proc', None)
       if proc and proc.poll() is None:
           proc.terminate()
           try:
               proc.wait(timeout=5)
           except Exception:
               proc.kill()
       self._weblink_proc = None
       self.next_media()
   ```

   Requirements / notes:
   - Install Chromium on the player image (`chromium-browser` on Raspberry Pi OS).
   - Ensure Chromium gets window focus over Kivy and is fully killed before the
     next item, including on pause/stop/restart paths and on app shutdown
     (`on_stop`) so no stray browser window is left behind.
   - On Wayland/X11 the player already sets `SDL_VIDEODRIVER`; verify Chromium
     launches on the same display/session.

2. **Embedded web view widget** (`kivy_garden.webview`, WebKit/GTK, or WebView2).
   Cleaner UX (stays inside the Kivy widget tree) but fragile and poorly
   supported on Pi/Wayland — only pursue if option 1 is unacceptable.

3. **Server-side screenshot fallback (no player change).** If embedding a live
   browser is not desirable, DigiServer can periodically screenshot the URL and
   store it as a normal `image` item; the player then needs no changes. This
   loses live/animated content. Documented here for completeness only.

---

## 4. Checklist for the player update

- [ ] `get_playlists_v2.py`: skip download when `type == 'weblink'`.
- [ ] `get_playlists_v2.py`: preserve `type` in the rewritten playlist items
      (fixes the current loss of `type`).
- [ ] `get_playlists_v2.py`: keep the original web `url` for weblink items.
- [ ] `main.py` `play_current_media()`: branch to `play_weblink()` **before** the
      `os.stat()` file check.
- [ ] `main.py`: implement `play_weblink(url, duration)` (Chromium kiosk).
- [ ] `main.py`: validate scheme is `http`/`https`; reject others.
- [ ] Kill/cleanup the browser process on next item, pause, stop, restart, and
      `on_stop`.
- [ ] Install Chromium on the player image / document it in the player README.
- [ ] Test: mixed playlist (image → video → weblink → image) cycles correctly
      and respects per-item `duration`.

---

## 5. Security notes

- Only allow `http`/`https` schemes on both server and player; never open
  `file://`, `chrome://`, etc.
- The server validates and stores the URL when the operator adds it; the player
  should still re-validate the scheme before launching the browser (defence in
  depth).
- Consider running Chromium with `--incognito` (no persistent cookies/cache) as
  shown above.
