# Jellyfin SRF Play Plugin

A Jellyfin plugin for accessing SRF Play (Swiss Radio and Television) video-on-demand content and live sports streaming.

## Status

**Beta/Alpha** - This plugin has been tested on two Jellyfin instances and is working. Some clients may experience issues with hardware decoding, which appears to be client-specific behavior.

## Quick Install

Add this repository URL in Jellyfin (Dashboard → Plugins → Repositories):

```
https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/raw/branch/master/manifest.json
```

Then install "SRF Play" from the plugin catalog.

## Features

- Access to SRF Play VOD content (video-on-demand, no DRM-protected content)
- **Live Sports Streaming** - Watch scheduled sports events (skiing, Formula 1, football, tennis, etc.)
- Support for all Swiss broadcasting units (SRF, RTS, RSI, RTR, SWI)
- Automatic content expiration handling
- Continue Watching cleanup — ended livestreams and abandoned playback don't pile up in the resume row
- Latest and trending content discovery
- Quality selection (Auto lets CDN decide, SD prefers 480p/360p, HD prefers 1080p/720p)
- HLS streaming support with Akamai token authentication
- Proxy support for routing traffic through alternate gateways
- Smart caching with reduced TTL for upcoming livestreams

## Screenshots

### Channel Menu
![SRF Play Menu](res/menu.png)

The main channel interface showing the content folders.
### Playback
![SRF Play Playback](res/playback.png)

Video playback with HLS streaming support and quality selection.

## Testing

The plugin includes comprehensive testing:

- **Unit tests** (xUnit framework) for core services
- **API spec validation tests** for all business units (SRF, RTS, RSI, RTR, SWI)
- **Integration testing** - Tested on multiple Jellyfin instances with VOD and live sports streaming

**Run tests:**
```bash
# All tests
dotnet test

# Unit tests only
dotnet test --filter "Category!=Integration&Category!=APISpec"

# API spec tests only
dotnet test --filter "Category=APISpec"

# With coverage
dotnet test --collect:"XPlat Code Coverage"
```

See [Test Documentation](Jellyfin.Plugin.SRFPlay.Tests/README.md) for more details.

## API Information

**Base URL:** `https://il.srgssr.ch/integrationlayer/2.0/`

### Key Endpoints

**Integration Layer API v2.0:**
- `GET /mediaComposition/byUrn/{urn}` - Get video metadata and playable URLs
- `GET /video/{businessUnit}/latest` - Get latest videos
- `GET /video/{businessUnit}/trending` - Get trending videos

**Play v3 API:**
- `GET /play/v3/api/{bu}/production/livestreams?eventType=SPORT` - Get scheduled sports livestreams
- `GET /play/v3/api/{bu}/production/shows/{id}` - Get show details
- `GET /play/v3/api/{bu}/production/videos-by-show-id?showId={id}` - Get episodes for a show

**Akamai Token Service:**
- `GET /akahd/token?acl=/{path}/*` - Authenticate stream URLs for playback

### URN Format

- `urn:{bu}:video:{id}` - Video URN (VOD content)
- `urn:{bu}:scheduled_livestream:video:{id}` - Scheduled sports livestream URN (supported)
- `urn:{bu}:video:livestream_{channel}` - Live TV channel URN (not supported due to Widevine DRM)

Examples:
- `urn:srf:video:c4927fcf-4ab4-4bcf-be4e-00e4ee9e6d6b` (VOD)
- `urn:srf:scheduled_livestream:video:7b31c9a6-a96e-4c0e-bfc2-f0d6237f2233` (Sports event)
- `urn:srf:video:c4927fcf-e1a0-0001-7edd-1ef01d441651` (SRF 1 Live - DRM protected)

## Building the Plugin

### Prerequisites

- .NET 8.0 SDK
- Jellyfin 10.9.11 or later

### Build Steps

```bash
cd Jellyfin.Plugin.SRFPlay
dotnet build
```

### Output

The compiled plugin will be in `bin/Debug/net8.0/`

## Manual Installation

1. Build the plugin (see above)
2. Copy the compiled DLL to your Jellyfin plugins directory
3. Restart Jellyfin
4. Configure the plugin in Jellyfin Dashboard → Plugins → SRF Play
5. The SRF Play channel will appear in Jellyfin with three main folders:
   - **Latest Videos** - Recently published content
   - **Trending Videos** - Popular content
   - **Live Sports & Events** - Scheduled sports livestreams (skiing, F1, football, etc.)

## Configuration

- **Business Unit**: Select the Swiss broadcasting unit (default: SRF)
- **Quality Preference**: Choose video quality — Auto (first available, CDN decides), SD, or HD
- **Content Refresh Interval**: How often to check for new content (1-168 hours)
- **Expiration Check Interval**: How often to check for expired content (1-168 hours)
- **Cache Duration**: How long to cache metadata (5-1440 minutes)
- **Enable Latest Content**: Automatically discover latest videos
- **Enable Trending Content**: Automatically discover trending videos
- **Proxy Settings**: Configure proxy server for routing SRF API traffic (optional)
  - **Use Proxy**: Enable/disable proxy usage
  - **Proxy Address**: Proxy server URL (e.g., http://proxy.example.com:8080)
  - **Proxy Username**: Optional authentication username
  - **Proxy Password**: Optional authentication password
- **Continue Watching Cleanup**: Stops stale SRF Play entries accumulating in the resume row
  - **Clean up stale resume points**: Enables the daily "Clean Up SRF Play Continue Watching" task (4 AM)
  - **Never keep a resume point for livestreams**: Clears the position the moment a livestream stops
  - **Maximum Resume Point Age**: Clear entries untouched for this many days (default 30, 0 disables)
  - **Minimum Resume Position**: Clear entries at or below this position (default 60s, 0 disables)
  - **Finished Threshold**: Clear entries at or beyond this share of the runtime (default 92%, 0 disables)

Clearing a resume point only resets the playback position. Nothing is deleted, the item stays
unwatched, and only SRF Play items are ever touched. The plugin config page also has
**Clean Up Stale Entries Now** and **Clear All SRF Play Entries** buttons for running it on demand.

For detailed proxy setup instructions, see [PROXY_SETUP_GUIDE.md](PROXY_SETUP_GUIDE.md).

## Troubleshooting

If you encounter issues with the plugin:
- Check [DEBUG_GUIDE.md](DEBUG_GUIDE.md) for detailed logging information
- Enable debug logging in Jellyfin to see detailed request/response information
- Common issues include DRM-protected content and geo-restrictions

## Technical Architecture

### Directory Structure

```
Jellyfin.Plugin.SRFPlay/
├── Api/
│   ├── Models/                    # API response models
│   │   ├── MediaComposition.cs    #   Root composition (HasChapters helper)
│   │   ├── Chapter.cs             #   Video/episode chapter with resources
│   │   ├── Resource.cs            #   Stream URL entry (IsPlayable helper)
│   │   ├── Show.cs
│   │   ├── Episode.cs
│   │   └── PlayV3/                # Play v3 API models
│   │       ├── PlayV3Show.cs
│   │       ├── PlayV3Topic.cs
│   │       ├── PlayV3Video.cs
│   │       ├── PlayV3TvProgram.cs
│   │       ├── PlayV3Response.cs
│   │       ├── PlayV3DirectResponse.cs
│   │       └── PlayV3DataContainer.cs
│   ├── SRFApiClient.cs            # HTTP client for SRF APIs
│   ├── ISRFApiClientFactory.cs    # Factory interface
│   └── SRFApiClientFactory.cs     # Factory implementation
├── Channels/
│   └── SRFPlayChannel.cs          # Channel implementation
├── Configuration/
│   ├── PluginConfiguration.cs
│   └── configPage.html
├── Constants/
│   └── ApiEndpoints.cs            # API base URLs and endpoint constants
├── Controllers/
│   └── StreamProxyController.cs   # HLS proxy endpoints (master/variant/segment)
├── Services/
│   ├── Interfaces/                # Service contracts
│   │   ├── IStreamProxyService.cs
│   │   ├── IStreamUrlResolver.cs
│   │   ├── IMediaCompositionFetcher.cs
│   │   ├── IMediaSourceFactory.cs
│   │   ├── IMetadataCache.cs
│   │   ├── IContentRefreshService.cs
│   │   ├── IContentExpirationService.cs
│   │   └── ICategoryService.cs
│   ├── StreamProxyService.cs      # HLS proxy: auth, manifest rewriting, segments
│   ├── StreamUrlResolver.cs       # Stream selection & Akamai authentication
│   ├── MediaCompositionFetcher.cs # Cached API fetcher
│   ├── MediaSourceFactory.cs      # Jellyfin MediaSourceInfo builder
│   ├── MetadataCache.cs           # Thread-safe ConcurrentDictionary cache
│   ├── ContentExpirationService.cs
│   ├── ContentRefreshService.cs
│   └── CategoryService.cs
├── Providers/
│   ├── SRFSeriesProvider.cs       # Series metadata
│   ├── SRFEpisodeProvider.cs      # Episode metadata
│   ├── SRFImageProvider.cs        # Image fetching
│   └── SRFMediaProvider.cs        # Playback URLs
├── Utilities/
│   ├── Extensions.cs              # BusinessUnit.ToLowerString() extension
│   ├── MimeTypeHelper.cs          # Content-type detection
│   ├── PlaceholderImageGenerator.cs
│   └── UrnHelper.cs               # URN parsing utilities
├── ScheduledTasks/
│   ├── ContentRefreshTask.cs      # Periodic content refresh
│   └── ExpirationCheckTask.cs     # Periodic expiration check
├── ServiceRegistrator.cs          # DI registration
└── Plugin.cs                      # Main plugin entry point
```

### Key Components

1. **API Client** (`SRFApiClient`): HTTP requests to SRF Integration Layer and Play v3 API, with proxy support
2. **Channel** (`SRFPlayChannel`): SRF Play channel with Latest, Trending, and Live Sports folders
3. **Stream Proxy** (`StreamProxyService` + `StreamProxyController`): HLS proxy that handles Akamai token auth, manifest URL rewriting, deferred authentication, and token refresh for both VOD and livestreams
4. **Stream Resolver** (`StreamUrlResolver`): Selects optimal HLS stream by quality preference, filters DRM content
5. **Metadata Cache** (`MetadataCache`): Thread-safe `ConcurrentDictionary` cache with dynamic TTL for livestreams
6. **Media Composition Fetcher** (`MediaCompositionFetcher`): Cached wrapper around API client for media composition requests
7. **Content Providers** (`SRFSeriesProvider`, `SRFEpisodeProvider`, `SRFImageProvider`, `SRFMediaProvider`): Jellyfin integration for series, episodes, images, and media sources
8. **Scheduled Tasks**: Automatic content refresh and expiration management
9. **Utilities**: Business unit extensions, MIME type helpers, URN parsing, placeholder image generation

## Important Notes

### Content Limitations

- **No DRM content**: Only non-DRM protected content is accessible (no Widevine/FairPlay)
- **No live TV channels**: Main channels (SRF 1, SRF zwei, SRF info) use DRM and are not supported
- **Sports livestreams supported**: Scheduled sports events (skiing, F1, football, etc.) work without DRM
- **Content expiration**: SRF content has validity periods, plugin tracks and removes expired content
- **Upcoming events**: Sports events appear before they start but require channel refresh to play once live
- **No subtitles**: Subtitle support not currently implemented

### Extensibility

The plugin is designed to support all Swiss broadcasting units:
- **SRF** (Schweizer Radio und Fernsehen - German)
- **RTS** (Radio Télévision Suisse - French)  
- **RSI** (Radiotelevisione svizzera - Italian)
- **RTR** (Radiotelevisiun Svizra Rumantscha - Romansh)
- **SWI** (Swiss World International)

Currently focused on SRF but easily extensible.

## Development

The plugin includes:
- Complete API integration with SRF Play (Integration Layer v2.0 and Play v3)
- **Live sports streaming** with scheduled event detection
- Channel with Latest, Trending, and Live Sports folders
- Metadata providers for series and episodes
- Image fetching and caching
- HLS stream playback with Akamai token authentication
- Automatic content expiration handling
- Scheduled tasks for content refresh
- Smart caching with dynamic TTL for upcoming livestreams

### Known Issues

- Some clients may experience issues with hardware decoding (appears to be client-specific)
- Some edge cases may need additional handling
- Performance optimization may be needed for very large content catalogs

### Contributing

Contributions welcome! This project is hosted on a self-hosted [Gitea](https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay) instance.

**You don't need a separate account** — you can sign in with your existing GitHub account. On the [sign-in page](https://gitea.tourolle.paris/user/login), choose **"Sign in with GitHub"** to register and log in via GitHub OAuth. Once signed in, you can:

- **Raise issues** — report bugs or request features on the [issue tracker](https://gitea.tourolle.paris/dtourolle/jellyfin-srfPlay/issues)
- **Contribute code** — fork the repository, push a branch, and open a pull request

## License

See LICENSE file for details.

## Acknowledgments

This plugin was developed partly using [Claude Code](https://docs.anthropic.com/en/docs/claude-code) by Anthropic.

Inspired by the excellent [Kodi SRG SSR addon](https://github.com/goggle/script.module.srgssr) by [@goggle](https://github.com/goggle), which served as a fantastic reference for understanding the SRG SSR API structure, authentication mechanisms, and handling of scheduled livestreams.

## References

- [SRF Play](https://www.srf.ch/play)
- [Jellyfin Plugin Documentation](https://jellyfin.org/docs/general/server/plugins/)
- [SRG SSR Integration Layer API](https://il.srgssr.ch/)
- [Kodi SRG SSR Addon](https://github.com/goggle/script.module.srgssr) - Reference implementation
