# Paragon TV - Backend & Frontend Contract

This document describes how the Android TV app processes the `/medias` API response. It covers every case the frontend handles so that backend and frontend stay aligned with no assumptions.

---

## API Endpoint

`GET /medias`

Auth: Bearer token via `Authorization` header.

---

## Expected Response Format

```json
{
  "status": true,
  "message": "Media list retrieved successfully",
  "version": "2026-04-12T10:30:00",
  "data": {
    "default": [ ...media items... ],
    "schedules": [ ...schedule objects... ]
  }
}
```

---

## Field Reference

### Top Level

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `status` | boolean | Yes | `true` for success |
| `message` | string | No | Human-readable message |
| `version` | string | Yes | **App skips re-downloading media if this hasn't changed.** Must change whenever any media or schedule is added/removed/modified. |
| `data` | object | Yes | Contains `default` and `schedules` |

### Media Item

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `id` | int | No | Not used by frontend, safe to include |
| `mediaUrl` | string | Yes | Full URL. Items with null/blank URL are **filtered out**. |
| `mediaType` | string | Yes | `"image"` or `"video"`. Anything other than `"video"` is treated as an image. |
| `mimeType` | string | No | Not used for playback decisions. Informational only. |
| `showForDelay` | int | No | Seconds to display an image. Default: **10 seconds** if null/0. **Ignored for videos** (videos play to completion). |
| `displayOrder` | int | No | Lower numbers play first. Items without this field go to the end. |
| `thumbnail` | string | No | Not used for playback. |

### Schedule

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `type` | string | Yes | `"standard"`, `"happy_hour"`, or `"special_announcement"` |
| `label` | string | No | Human-readable name. Logged for debugging. |
| `startTime` | string | Yes | Format: `"HH:mm"` (24-hour). e.g., `"07:00"`, `"22:30"` |
| `endTime` | string | Yes | Format: `"HH:mm"` (24-hour). Schedule is active from startTime up to (but not including) endTime. |
| `priority` | int | No | Higher number = higher priority. Default: 0 if null. |
| `validFrom` | string | No | Format: `"YYYY-MM-DD"`. Schedule ignored before this date. No limit if null. |
| `validUntil` | string | No | Format: `"YYYY-MM-DD"`. Schedule ignored after this date. No limit if null. |
| `createdAt` | string | No | ISO 8601 format (e.g., `"2026-04-10T14:30:00+00:00"`). Used as final tiebreaker for overlapping schedules. Newer wins. |
| `media` | array | Yes | List of media items for this schedule. |

---

## How the App Processes the Response

### Step 1: Date filtering

Each schedule is checked against today's date:
- If `validFrom` is set and today is before it → schedule is excluded
- If `validUntil` is set and today is after it → schedule is excluded
- If both are null → schedule is always date-valid

### Step 2: Time filtering

Remaining schedules are checked against the current time:
- Active if current time >= `startTime` AND current time < `endTime`
- **Midnight crossing supported**: if `startTime` > `endTime` (e.g., 22:00 - 02:00), the schedule is active if current time >= startTime OR current time < endTime

### Step 3: Pick the winner

If multiple schedules pass both filters, the app picks one using this priority:

1. **`priority`** — higher number wins
2. **`type` weight** — `happy_hour` (3) > `special_announcement` (2) > `standard` (1)
3. **`createdAt`** — newer timestamp wins (string comparison, so ISO 8601 format is required)

### Step 4: Determine media list

- If a winning schedule exists → use its `media` list
- If no schedule matches → use `default` list
- Items with null/blank `mediaUrl` are filtered out
- List is sorted by `displayOrder` (ascending, nulls go last)

### Step 5: Schedule auto-transition timer

The app calculates the next time a schedule change could happen (e.g., a schedule ending or starting) and sets an internal timer. When the timer fires, Steps 1-4 run again automatically. **No API call is made** — it re-evaluates the cached data.

The timer is capped at 60 minutes. At midnight, a re-evaluation always happens (date boundaries may change `validFrom`/`validUntil` filtering).

---

## Case-by-Case Behavior

### Case 1: Only `default` has media, `schedules` is empty

Default media loops all day, every day.

### Case 2: Only `schedules` have media, `default` is empty

During active schedule times → that schedule's media plays.
Outside all schedule times → TV shows a static Paragon logo screen (empty state).

### Case 3: Both `default` and `schedules` are empty

TV shows a static Paragon logo screen.

### Case 4: API call fails (network error, 500, timeout)

The app retries up to **3 times** with delays of 5s, 15s, 30s. If all fail:
- If there was previously cached data → the cached content keeps playing
- If no cached data exists → TV shows Paragon logo with "Unable to load media. Check network connection."

### Case 5: API returns 401

The app logs out, clears the token, and restarts to the registration screen.

### Case 6: Same media URL in both `default` and a schedule

Downloaded and stored only once. No duplication.

### Case 7: Media removed from API response

On next successful sync, the local file is **deleted automatically**. No stale media remains on the device.

### Case 8: `version` field unchanged

The app **skips the entire sync** (no media downloads, no file deletions). This is an optimization — make sure `version` changes whenever content changes.

### Case 9: Schedule is shorter than a video it contains

Example: 10-minute schedule with a 20-minute video. The video gets **cut off** when the schedule ends. The app does not wait for video completion — schedule boundaries are strict.

### Case 10: Overlapping schedules with identical priority, type, and time

The `createdAt` field breaks the tie — newest wins. If `createdAt` is also the same or null, the last item in the API response array wins (non-deterministic from backend perspective).

**Recommendation:** Avoid this scenario. Always assign different priorities to overlapping schedules.

### Case 11: Unsupported media format

The app attempts to play it. If it fails:
- **Image:** Logs error to Firebase, waits 1 second, skips to next item
- **Video:** Logs error to Firebase, skips to next item immediately

No error is shown to the viewer. The broken item is silently skipped.

### Case 12: Single media item in the active list

- **Single image:** Displays indefinitely (loops with its `showForDelay` duration)
- **Single video:** Loops/repeats continuously

### Case 13: Schedule `startTime` equals `endTime`

The schedule will never be active (no time window matches). Effectively dead.

### Case 14: `showForDelay` is 0 or null for an image

Falls back to **10 seconds** display time.

### Case 15: `displayOrder` is null or missing

The item is placed at the **end** of the list, after all items that have a `displayOrder` value.

---

## Overlapping Schedule Resolution — Detailed Examples

### Example A: Different priorities

| Schedule | Time | Priority | Type |
|----------|------|----------|------|
| Daily Menu | 12:00 - 22:00 | 1 | standard |
| Flash Promo | 13:00 - 14:00 | 5 | standard |

- 12:00 - 12:59 → Daily Menu
- 13:00 - 13:59 → Flash Promo (higher priority)
- 14:00 - 21:59 → Daily Menu

### Example B: Same priority, different types

| Schedule | Time | Priority | Type |
|----------|------|----------|------|
| Lunch | 11:00 - 14:00 | 1 | standard |
| Happy Hour | 13:00 - 17:00 | 1 | happy_hour |

- 11:00 - 12:59 → Lunch
- 13:00 - 13:59 → Happy Hour (happy_hour type outweighs standard)
- 14:00 - 16:59 → Happy Hour

### Example C: Same priority, same type, different createdAt

| Schedule | Time | Priority | Type | Created |
|----------|------|----------|------|---------|
| Promo V1 | 12:00 - 15:00 | 1 | standard | Apr 1 |
| Promo V2 | 12:00 - 15:00 | 1 | standard | Apr 10 |

- 12:00 - 14:59 → Promo V2 (newer `createdAt`)

---

## Push Notification Refresh

When the backend sends a push notification (via OneSignal/FCM), the app immediately re-fetches the `/medias` endpoint and syncs. Supported push actions:

- `refresh` — full re-fetch
- `reordered` — media order changed
- `added` — new media added
- `deleted` — media removed

After processing, the app calls `/acknowledge-notification` to confirm receipt.

---

## Media Recommendations for Backend

### Video

- **Format:** MP4 with H.264 codec (best compatibility)
- **Resolution:** 1080p max. Do not serve 4K — most TV devices are 1080p and will waste resources decoding 4K for no visual benefit. Can cause stuttering on low-end devices (Fire TV Stick).
- **Avoid:** `.mov` files (limited support), ProRes codec, HEVC on older devices

### Images

- **Format:** JPEG or PNG (best compatibility). WebP is also supported.
- **Resolution:** Match the target display (1920x1080 for Full HD TVs)

### General

- Always serve media over HTTPS
- URLs must be direct file links (not redirects to login pages or HTML wrappers)
- Keep file sizes reasonable — all media is downloaded to device storage

---

## Version Field Contract

The `version` field is critical for performance. The app uses it to avoid unnecessary downloads.

**Rules:**
- Must change whenever anything in the response changes (media added/removed/reordered, schedule modified, etc.)
- If `version` is the same as the last sync, the app skips all processing
- Recommended format: timestamp (e.g., `"2026-04-12T10:30:00"`) or incrementing string

**If `version` is not updated when content changes, the TV will show stale content until the next forced refresh (push notification or manual remote button press).**

---

## Error Handling Summary

| Scenario | App Behavior |
|----------|-------------|
| API returns success with media | Downloads new files, deletes removed files, plays content |
| API returns success but empty | Shows Paragon logo (empty state) |
| API returns error / network failure | Retries 3 times, falls back to cached content, shows error if no cache |
| API returns 401 | Logs out, shows registration screen |
| Unsupported media format | Silently skips, logs to Firebase |
| Video longer than schedule window | Video cut off at schedule boundary |
| `version` unchanged | Skips sync entirely |
