Compare commits

..
7 Commits
Author SHA1 Message Date
dtourolle 8e86db100a tokens refresh on media start
🏗️ Build Plugin / build (push) Successful in 2m19s
🧪 Test Plugin / test (push) Successful in 1m8s
🚀 Release Plugin / build-and-release (push) Successful in 2m18s
2025-11-15 17:51:14 +01:00
dtourolle d48b515898 Fix -- Use validTo time for live streams
🏗️ Build Plugin / build (push) Successful in 2m22s
🧪 Test Plugin / test (push) Successful in 1m6s
2025-11-15 16:09:46 +01:00
dtourolle 31b2402a96 more readme refinement
🏗️ Build Plugin / build (push) Successful in 2m22s
🧪 Test Plugin / test (push) Successful in 1m7s
2025-11-14 22:01:17 +01:00
dtourolle 3691b4e30e removed md unrelated to current state 2025-11-14 21:50:58 +01:00
dtourolle 87ccf80813 remove netowkring notes ans script
🏗️ Build Plugin / build (push) Successful in 2m25s
🧪 Test Plugin / test (push) Successful in 1m8s
2025-11-14 21:43:43 +01:00
dtourolle 6ed53054c3 remove changelog
🏗️ Build Plugin / build (push) Successful in 2m18s
🧪 Test Plugin / test (push) Successful in 1m10s
🚀 Release Plugin / build-and-release (push) Successful in 2m16s
2025-11-14 21:39:34 +01:00
dtourolle 1a8b7a33ef more dman ci stuff
🏗️ Build Plugin / build (push) Successful in 2m22s
🧪 Test Plugin / test (push) Successful in 1m8s
🚀 Release Plugin / build-and-release (push) Failing after 2m18s
2025-11-14 21:29:51 +01:00
11 changed files with 128 additions and 683 deletions
+51 -25
View File
@@ -70,33 +70,59 @@ jobs:
echo "artifact_name=${ARTIFACT_NAME}" >> $GITHUB_OUTPUT echo "artifact_name=${ARTIFACT_NAME}" >> $GITHUB_OUTPUT
echo "Found artifact: ${ARTIFACT}" echo "Found artifact: ${ARTIFACT}"
- name: Generate changelog - name: Create Release
id: changelog env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
# Get commits since last tag # Get repository information
PREV_TAG=$(git tag --sort=-creatordate | grep -v "${{ steps.get_version.outputs.version }}" | head -n 1) REPO_OWNER="${{ github.repository_owner }}"
if [ -z "$PREV_TAG" ]; then REPO_NAME="${{ github.event.repository.name }}"
CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges) GITEA_URL="${{ github.server_url }}"
# Prepare release body
RELEASE_BODY="SRFPlay Jellyfin Plugin ${{ steps.get_version.outputs.version }}\n\nSee attached files for plugin installation."
RELEASE_BODY_JSON=$(echo -n "${RELEASE_BODY}" | jq -Rs .)
# Create release using Gitea API
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" \
-d "{
\"tag_name\": \"${{ steps.get_version.outputs.version }}\",
\"name\": \"Release ${{ steps.get_version.outputs.version }}\",
\"body\": ${RELEASE_BODY_JSON},
\"draft\": false,
\"prerelease\": false
}")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
RELEASE_ID=$(echo "$BODY" | jq -r '.id')
echo "Created release with ID: ${RELEASE_ID}"
else else
CHANGELOG=$(git log ${PREV_TAG}..${{ steps.get_version.outputs.version }} --pretty=format:"- %s (%h)" --no-merges) echo "Failed to create release. HTTP ${HTTP_CODE}"
echo "$BODY"
exit 1
fi fi
# Save to file for release notes # Upload plugin artifact
echo "## What's Changed" > RELEASE_NOTES.md echo "Uploading plugin artifact..."
echo "" >> RELEASE_NOTES.md curl -f -X POST \
echo "${CHANGELOG}" >> RELEASE_NOTES.md -H "Authorization: token ${GITEA_TOKEN}" \
echo "" >> RELEASE_NOTES.md -H "Content-Type: application/zip" \
echo "**Full Changelog**: ${PREV_TAG}...${{ steps.get_version.outputs.version }}" >> RELEASE_NOTES.md --data-binary "@${{ steps.jprm.outputs.artifact }}" \
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=${{ steps.jprm.outputs.artifact_name }}"
- name: Create Release # Upload build.yaml
uses: actions/gitea-release-action@v1 echo "Uploading build.yaml..."
with: curl -f -X POST \
token: ${{ secrets.GITHUB_TOKEN }} -H "Authorization: token ${GITEA_TOKEN}" \
tag_name: ${{ steps.get_version.outputs.version }} -H "Content-Type: application/x-yaml" \
name: Release ${{ steps.get_version.outputs.version }} --data-binary "@build.yaml" \
body_path: RELEASE_NOTES.md "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=build.yaml"
draft: false
prerelease: false echo "✅ Release created successfully!"
files: | echo "View at: ${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/tag/${{ steps.get_version.outputs.version }}"
${{ steps.jprm.outputs.artifact }}
build.yaml
-88
View File
@@ -1,88 +0,0 @@
# Compilation Fixes - COMPLETED ✅
## Status: ALL ERRORS RESOLVED
**Build Status:** ✅ SUCCESS
The plugin now compiles successfully with no errors!
## Fixed Issues
### 1. Collection Type Warnings ✅
**Issue:** CA2227 and CA1002 - Collection properties should be read-only and use appropriate collection types
**Files Fixed:**
- `Api/Models/MediaComposition.cs` - Changed `List<Chapter>` to `IReadOnlyList<Chapter>`
- `Api/Models/Chapter.cs` - Changed `List<Resource>` to `IReadOnlyList<Resource>`
**Solution:** Used `IReadOnlyList<T>` to satisfy code analysis while maintaining JSON deserialization compatibility.
### 2. MetadataCache Warnings ✅
**Issue:** Multiple issues with MetadataCache
- CA1001: Type should implement IDisposable (owns ReaderWriterLockSlim)
- MT1012: Lock acquisition should be wrapped in try blocks
- CA1852: CacheEntry class should be sealed
**File Fixed:** `Services/MetadataCache.cs`
**Solution:**
- Implemented IDisposable interface
- Wrapped all lock acquisitions in try-catch blocks
- Added ObjectDisposedException handling
- Sealed the CacheEntry inner class
- Reordered fields (readonly fields before non-readonly)
### 3. SRFMediaProvider Warnings ✅
**Issue:**
- SA1648: inheritdoc should be used with inheriting class
- CA1849: Avoid synchronous blocking
**File Fixed:** `Providers/SRFMediaProvider.cs`
**Solution:**
- Replaced `/// <inheritdoc />` with proper XML documentation summaries
- Changed from `Task.Wait()` and `.Result` to `.GetAwaiter().GetResult()` (less problematic)
### 4. ContentExpirationService Warnings ✅
**Issue:** SA1028 - Trailing whitespace
**File Fixed:** `Services/ContentExpirationService.cs`
**Solution:** Removed trailing whitespace on lines 79 and 221
### 5. CA1826 Warnings ✅
**Issue:** Use indexer instead of LINQ `.First()` for collections with indexers
**Files Fixed:**
- `Services/ContentExpirationService.cs`
- `Providers/SRFEpisodeProvider.cs`
- `Providers/SRFImageProvider.cs`
- `Providers/SRFMediaProvider.cs`
**Solution:** Replaced `.First()` calls with `[0]` indexer access for IReadOnlyList collections
## Build Output
```
Build succeeded in 1.0s
Jellyfin.Plugin.SRFPlay succeeded → Jellyfin.Plugin.SRFPlay/bin/Debug/net8.0/Jellyfin.Plugin.SRFPlay.dll
```
## Summary
All 17 initial compilation errors have been resolved:
- ✅ 4 collection property warnings
- ✅ 6 MetadataCache warnings
- ✅ 3 SRFMediaProvider warnings
- ✅ 2 ContentExpirationService whitespace warnings
- ✅ 6 CA1826 indexer warnings
- ✅ 1 field ordering warning
The plugin is now ready for testing with a Jellyfin instance!
## Next Steps
1. ✅ Plugin compiles successfully
2. ⏭️ Test with Jellyfin instance
3. ⏭️ Verify content discovery
4. ⏭️ Test playback functionality
5. ⏭️ Validate expiration handling
@@ -61,7 +61,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
public string Description => "Swiss Radio and Television video-on-demand content"; public string Description => "Swiss Radio and Television video-on-demand content";
/// <inheritdoc /> /// <inheritdoc />
public string DataVersion => "1.0"; public string DataVersion => "1.1";
/// <inheritdoc /> /// <inheritdoc />
public string HomePageUrl => "https://www.srf.ch/play"; public string HomePageUrl => "https://www.srf.ch/play";
@@ -231,7 +231,8 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
.Where(p => p.Urn != null && .Where(p => p.Urn != null &&
!string.IsNullOrEmpty(p.Title) && !string.IsNullOrEmpty(p.Title) &&
p.ValidFrom != null && p.ValidFrom != null &&
p.ValidFrom.Value.ToUniversalTime() <= weekFromNow) p.ValidFrom.Value.ToUniversalTime() <= weekFromNow &&
(p.ValidTo == null || p.ValidTo.Value.ToUniversalTime() > now))
.OrderBy(p => p.ValidFrom) .OrderBy(p => p.ValidFrom)
.ToList(); .ToList();
@@ -432,36 +433,34 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
// Generate deterministic GUID from URN // Generate deterministic GUID from URN
var itemId = UrnToGuid(urn); var itemId = UrnToGuid(urn);
// Get stream URL and authenticate it // Get stream URL (unauthenticated - token will be added at playback time by SRFMediaProvider)
var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference); var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference);
// For scheduled livestreams that haven't started, streamUrl might be null // Skip scheduled livestreams that haven't started yet (no stream URL available)
var isUpcomingLivestream = chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl); if (chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl))
if (!string.IsNullOrEmpty(streamUrl))
{ {
// Authenticate the stream URL (required for all SRF streams, especially livestreams) _logger.LogDebug(
streamUrl = await _streamResolver.GetAuthenticatedStreamUrlAsync(streamUrl, cancellationToken).ConfigureAwait(false); "URN {Urn}: Skipping upcoming livestream '{Title}' - stream not yet available (starts at {ValidFrom})",
}
else if (isUpcomingLivestream)
{
// Use a placeholder for upcoming events
streamUrl = "http://placeholder.local/upcoming.m3u8";
_logger.LogInformation(
"URN {Urn}: Upcoming livestream '{Title}' - stream will be available at {ValidFrom}",
urn, urn,
chapter.Title, chapter.Title,
chapter.ValidFrom); chapter.ValidFrom);
continue;
} }
// Build overview with event time for upcoming livestreams // Skip items without a valid stream URL
var overview = chapter.Description ?? chapter.Lead; if (string.IsNullOrEmpty(streamUrl))
if (isUpcomingLivestream && chapter.ValidFrom != null)
{ {
var eventStart = chapter.ValidFrom.Value.ToString("dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture); _logger.LogWarning(
overview = $"[Upcoming Event - Starts at {eventStart}]\n\n{overview}"; "URN {Urn}: Skipping '{Title}' - no valid stream URL available",
urn,
chapter.Title);
noStreamCount++;
continue;
} }
// Build overview
var overview = chapter.Description ?? chapter.Lead;
// Get image URL - prefer chapter image, fall back to show image if available // Get image URL - prefer chapter image, fall back to show image if available
var imageUrl = chapter.ImageUrl; var imageUrl = chapter.ImageUrl;
if (string.IsNullOrEmpty(imageUrl) && mediaComposition.Show != null) if (string.IsNullOrEmpty(imageUrl) && mediaComposition.Show != null)
@@ -475,9 +474,10 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
_logger.LogWarning("URN {Urn}: No image URL available for '{Title}'", urn, chapter.Title); _logger.LogWarning("URN {Urn}: No image URL available for '{Title}'", urn, chapter.Title);
} }
// Use ValidFrom for premiere date if this is an upcoming livestream, otherwise use Date // Use ValidFrom for premiere date if this is a scheduled livestream, otherwise use Date
var premiereDate = isUpcomingLivestream ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime(); var premiereDate = chapter.Type == "SCHEDULED_LIVESTREAM" ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime();
// Store unauthenticated URL as placeholder - SRFMediaProvider will provide authenticated version at playback
var item = new ChannelItemInfo var item = new ChannelItemInfo
{ {
Id = itemId, Id = itemId,
@@ -501,14 +501,14 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
{ {
Id = itemId, Id = itemId,
Name = chapter.Title, Name = chapter.Title,
Path = streamUrl, Path = streamUrl, // Unauthenticated URL - placeholder only
Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http, Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http,
Container = "m3u8", Container = "m3u8",
SupportsDirectStream = true, SupportsDirectPlay = false, // Disable direct play - requires auth from provider
SupportsDirectPlay = true, SupportsDirectStream = false, // Disable direct stream - requires auth from provider
SupportsTranscoding = true, SupportsTranscoding = false, // Force use of IMediaSourceProvider
IsRemote = true, IsRemote = true,
Type = MediaBrowser.Model.Dto.MediaSourceType.Default, Type = MediaBrowser.Model.Dto.MediaSourceType.Placeholder, // Mark as placeholder
VideoType = VideoType.VideoFile VideoType = VideoType.VideoFile
} }
} }
@@ -119,37 +119,11 @@ public class SRFMediaProvider : IMediaSourceProvider
// Get stream URL based on quality preference // Get stream URL based on quality preference
var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference); var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference);
// Check if this is an upcoming livestream that hasn't started yet // For scheduled livestreams, always fetch fresh data to ensure stream URL is current
var isUpcomingLivestream = chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl); if (chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl))
if (string.IsNullOrEmpty(streamUrl) && !isUpcomingLivestream)
{ {
_logger.LogWarning("Could not resolve stream URL for URN: {Urn}", urn); _logger.LogDebug("URN {Urn}: Scheduled livestream has no stream URL, fetching fresh data", urn);
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources);
}
// For upcoming livestreams, check if the event has started
if (isUpcomingLivestream)
{
if (chapter.ValidFrom != null)
{
var eventStart = chapter.ValidFrom.Value.ToUniversalTime();
var now = DateTime.UtcNow;
if (eventStart > now)
{
_logger.LogInformation(
"URN {Urn}: Livestream '{Title}' hasn't started yet (starts at {ValidFrom}). User should refresh when live.",
urn,
chapter.Title,
chapter.ValidFrom);
// Return empty sources - event not yet playable
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources);
}
}
// Event should be live now - re-fetch media composition without cache
using var freshApiClient = new SRFApiClient(_loggerFactory); using var freshApiClient = new SRFApiClient(_loggerFactory);
var freshMediaComposition = freshApiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).GetAwaiter().GetResult(); var freshMediaComposition = freshApiClient.GetMediaCompositionByUrnAsync(urn, cancellationToken).GetAwaiter().GetResult();
@@ -163,19 +137,15 @@ public class SRFMediaProvider : IMediaSourceProvider
// Update cache with fresh data // Update cache with fresh data
_metadataCache.SetMediaComposition(urn, freshMediaComposition); _metadataCache.SetMediaComposition(urn, freshMediaComposition);
chapter = freshChapter; chapter = freshChapter;
_logger.LogInformation("URN {Urn}: Livestream is now live, got fresh stream URL", urn); _logger.LogInformation("URN {Urn}: Got fresh stream URL for scheduled livestream", urn);
}
else
{
_logger.LogWarning("URN {Urn}: Livestream should be live but still no stream URL available", urn);
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources);
} }
} }
else }
{
_logger.LogWarning("URN {Urn}: Failed to fetch fresh media composition for livestream", urn); if (string.IsNullOrEmpty(streamUrl))
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources); {
} _logger.LogWarning("Could not resolve stream URL for URN: {Urn}", urn);
return Task.FromResult<IEnumerable<MediaSourceInfo>>(sources);
} }
// Authenticate the stream URL (required for all SRF streams, especially livestreams) // Authenticate the stream URL (required for all SRF streams, especially livestreams)
@@ -188,13 +158,13 @@ public class SRFMediaProvider : IMediaSourceProvider
// Create media source // Create media source
var mediaSource = new MediaSourceInfo var mediaSource = new MediaSourceInfo
{ {
Id = urn, Id = item.Id.ToString(), // Use item GUID, not URN string (required for transcoding)
Name = chapter.Title, Name = chapter.Title,
Path = streamUrl, Path = streamUrl,
Protocol = MediaProtocol.Http, Protocol = MediaProtocol.Http,
Container = "m3u8", Container = "m3u8",
SupportsDirectStream = true, SupportsDirectStream = true,
SupportsDirectPlay = true, SupportsDirectPlay = false, // Disabled: auth tokens don't carry over to HLS segments in browser
SupportsTranscoding = true, SupportsTranscoding = true,
IsRemote = true, IsRemote = true,
Type = MediaSourceType.Default, Type = MediaSourceType.Default,
@@ -144,23 +144,6 @@ public class StreamUrlResolver : IDisposable
/// <returns>True if playable content is available.</returns> /// <returns>True if playable content is available.</returns>
public bool HasPlayableContent(Chapter chapter) public bool HasPlayableContent(Chapter chapter)
{ {
// For scheduled livestreams that haven't started yet, resources won't exist
// but we still want to show them. The stream will become available when the event starts.
if (chapter?.Type == "SCHEDULED_LIVESTREAM")
{
var now = DateTime.UtcNow;
var hasStarted = chapter.ValidFrom == null || chapter.ValidFrom.Value.ToUniversalTime() <= now;
if (!hasStarted)
{
_logger.LogInformation(
"Scheduled livestream '{Title}' hasn't started yet (starts at {ValidFrom}), will be playable when live",
chapter.Title,
chapter.ValidFrom);
return true; // Show it, stream will be available when event starts
}
}
if (chapter?.ResourceList == null || chapter.ResourceList.Count == 0) if (chapter?.ResourceList == null || chapter.ResourceList.Count == 0)
{ {
return false; return false;
-227
View File
@@ -1,227 +0,0 @@
# Network-Level Gateway Routing for SRF Content
This guide explains how to configure network-level routing to direct all SRF-related traffic (including video streams) through your Swiss gateway/proxy at 192.168.1.37.
## Overview
Instead of configuring proxy support at the application level, this approach uses Linux policy-based routing to redirect traffic destined for SRF domains through an alternate gateway. This ensures:
- API requests to `il.srgssr.ch` go through the gateway
- Video stream requests to `srf-vod-amd.akamaized.net` (and other CDNs) go through the gateway
- ffprobe and ffmpeg automatically use the gateway
- No application configuration needed - transparent to Jellyfin
## Prerequisites
- Root access to the Jellyfin server (192.168.1.4)
- Gateway at 192.168.1.37 with IPv4 forwarding enabled
- Both machines on the same network segment
## Installation on Jellyfin Server
### Step 1: Copy Scripts to Jellyfin Server
From your development machine:
```bash
# Copy the routing scripts to Jellyfin server
scp setup-gateway-routing.sh cleanup-gateway-routing.sh user@192.168.1.4:~
```
### Step 2: SSH to Jellyfin Server
```bash
ssh user@192.168.1.4
```
### Step 3: Make Scripts Executable
```bash
chmod +x setup-gateway-routing.sh cleanup-gateway-routing.sh
```
### Step 4: Run Setup Script
```bash
sudo ./setup-gateway-routing.sh
```
When prompted:
- **Gateway IP**: `192.168.1.37`
- **Network interface**: Find your interface name first with `ip -br link show` (common names: `eth0`, `ens18`, `enp0s3`)
The script will:
1. Create a custom routing table named `srf_gateway`
2. Resolve IP addresses for all SRF domains
3. Add routes through your gateway (192.168.1.37)
4. Create routing rules for policy-based routing
5. Set up a systemd service for persistence across reboots
### Step 5: Verify Routing
Check that routes are configured:
```bash
# Show the custom routing table
ip route show table srf_gateway
# Show routing rules
ip rule show | grep srf_gateway
# Test routing for Integration Layer API
ip route get $(dig +short il.srgssr.ch | head -1)
# Test routing for video CDN
ip route get $(dig +short srf-vod-amd.akamaized.net | head -1)
```
### Step 6: Test from Jellyfin Server
Test that the routing is working:
```bash
# Test API access
curl -v "https://il.srgssr.ch/integrationlayer/2.0/mediaComposition/byUrn/urn:srf:video:b84713f0-f81b-460f-9b0f-d0517310fb4f.json" 2>&1 | grep -E "(x-location|HTTP/)"
# Should show: x-location: CH
```
### Step 7: Restart Jellyfin
```bash
sudo systemctl restart jellyfin
```
## What Gets Routed
The following domains are routed through the gateway:
- `il.srgssr.ch` - Integration Layer API (metadata)
- `www.srf.ch` - Main SRF site
- `www.rts.ch` - RTS (Radio Télévision Suisse)
- `www.rsi.ch` - RSI (Radiotelevisione svizzera)
- `www.rtr.ch` - RTR (Radiotelevisiun Svizra Rumantscha)
- `www.swi.ch` - SWI (swissinfo)
- `srf-vod-amd.akamaized.net` - SRF video CDN
- `rts-vod-amd.akamaized.net` - RTS video CDN
- `rsi-vod-amd.akamaized.net` - RSI video CDN
- `play-web.srf.ch` - Play web interface
- `il-stage.srgssr.ch` - Staging environment
## How It Works
1. **DNS Resolution**: Domains are resolved to IP addresses
2. **Routing Table**: A custom routing table (`srf_gateway`) is created with routes through the gateway
3. **Policy Routing**: Rules direct traffic to specific IPs to use the custom routing table
4. **Persistence**: A systemd service ensures routes survive reboots
## Disabling Plugin Proxy Configuration
Once network-level routing is working, you can disable the proxy configuration in the plugin:
1. Go to Jellyfin Dashboard → Plugins → SRF Play
2. Uncheck "Use Proxy"
3. Save configuration
4. Restart Jellyfin
The plugin will use direct HTTP requests, but the network layer will transparently route them through the gateway.
## Troubleshooting
### Routes Not Working
Check if gateway is reachable:
```bash
ping 192.168.1.37
nc -zv 192.168.1.37 3128
```
Check routing table:
```bash
ip route show table srf_gateway
```
### DNS Changes
If SRF changes their IP addresses, you may need to re-run the setup script:
```bash
sudo ./cleanup-gateway-routing.sh
sudo ./setup-gateway-routing.sh
```
### Verify Traffic Path
Use `traceroute` to see the path:
```bash
traceroute $(dig +short il.srgssr.ch | head -1)
# Should show 192.168.1.37 as first hop
```
### Check Systemd Service
```bash
systemctl status srf-gateway-routing.service
journalctl -u srf-gateway-routing.service
```
## Removing the Configuration
To completely remove the routing configuration:
```bash
sudo ./cleanup-gateway-routing.sh
```
This will:
- Remove all routing rules
- Flush the custom routing table
- Disable and remove the systemd service
## Advantages of This Approach
1. **Transparent**: No application changes needed
2. **Complete Coverage**: All network traffic to SRF domains uses gateway
3. **Persistent**: Survives reboots
4. **Centralized**: Managed at network level
5. **Debug-Friendly**: Can verify with standard network tools
## Disadvantages
1. **DNS Changes**: If SRF changes IPs, routing must be updated
2. **New Domains**: New CDN domains require script update
3. **Static IPs Only**: Doesn't work with wildcard domains
## Gateway Configuration
Ensure your gateway (192.168.1.37) has:
1. **IPv4 Forwarding Enabled**:
```bash
# On the gateway (192.168.1.37)
sudo sysctl net.ipv4.ip_forward=1
# Make permanent
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
```
2. **Firewall Rules** (if using iptables):
```bash
# On the gateway (192.168.1.37)
sudo iptables -A FORWARD -s 192.168.1.4 -j ACCEPT
sudo iptables -A FORWARD -d 192.168.1.4 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 192.168.1.4 -o <outbound-interface> -j MASQUERADE
```
3. **Squid Proxy** (if using proxy mode):
- Already configured and accessible at port 3128
- Note: With network-level routing, traffic goes through the gateway's routing, not necessarily the Squid proxy
## Testing Complete Data Chain
After setup, test the complete flow:
1. **Metadata API**: Browse shows in Jellyfin - should work
2. **Video Playback**: Try playing a video - should work
3. **Thumbnails**: Images should load
All traffic should be routed through 192.168.1.37, giving you the Swiss location needed to access geo-blocked content.
-197
View File
@@ -1,197 +0,0 @@
# Proxy Configuration Guide for SRF Play Plugin
This guide explains how to configure the Jellyfin SRF Play plugin to route all API traffic through a proxy or alternate gateway.
## Overview
The SRF Play plugin now supports proxy configuration directly in the plugin settings. This allows you to:
- Route traffic through a specific gateway or proxy server
- Use authentication if your proxy requires it
- Bypass geo-restrictions or network policies
- Route only SRF-related traffic without affecting other Jellyfin operations
## Supported Proxy Types
The plugin supports:
- **HTTP proxies**: `http://proxy.example.com:8080`
- **HTTPS proxies**: `https://proxy.example.com:8443`
- **SOCKS5 proxies**: `socks5://proxy.example.com:1080`
## Configuration Steps
### 1. Access Plugin Settings
1. Open Jellyfin Dashboard
2. Navigate to **Dashboard → Plugins → SRF Play**
3. Scroll down to the **Proxy Settings** section
### 2. Configure Proxy
Fill in the following fields:
#### Use Proxy
- **Enable this checkbox** to route all SRF API requests through the proxy
#### Proxy Address
- Enter your proxy server address with protocol and port
- Examples:
- `http://192.168.1.100:8080`
- `http://proxy.example.com:3128`
- `socks5://127.0.0.1:1080`
#### Proxy Username (Optional)
- Enter username if your proxy requires authentication
- Leave empty if no authentication is needed
#### Proxy Password (Optional)
- Enter password if your proxy requires authentication
- Leave empty if no authentication is needed
### 3. Save Configuration
1. Click **Save** button
2. Restart Jellyfin to apply changes (recommended)
## Examples
### Example 1: Simple HTTP Proxy (No Authentication)
```
Use Proxy: ✓ Enabled
Proxy Address: http://192.168.1.1:8080
Proxy Username: (empty)
Proxy Password: (empty)
```
### Example 2: Authenticated HTTP Proxy
```
Use Proxy: ✓ Enabled
Proxy Address: http://proxy.company.com:3128
Proxy Username: myusername
Proxy Password: mypassword
```
### Example 3: SOCKS5 Proxy
```
Use Proxy: ✓ Enabled
Proxy Address: socks5://127.0.0.1:1080
Proxy Username: (empty)
Proxy Password: (empty)
```
## Setting Up a Transparent Proxy Gateway on Ubuntu
If you want to create your own transparent proxy gateway on Ubuntu, here are some options:
### Option A: Squid Proxy
Install and configure Squid as a transparent proxy:
```bash
# Install Squid
sudo apt update
sudo apt install squid
# Edit configuration
sudo nano /etc/squid/squid.conf
# Add these lines:
http_port 3128
acl localnet src 192.168.1.0/24
http_access allow localnet
# Restart Squid
sudo systemctl restart squid
```
Then in plugin settings:
```
Proxy Address: http://192.168.1.1:3128
```
### Option B: SSH Tunnel (SOCKS5)
Create a SOCKS5 proxy through SSH:
```bash
# On your local machine
ssh -D 1080 -N user@remote-gateway-server
```
Then in plugin settings:
```
Proxy Address: socks5://127.0.0.1:1080
```
### Option C: Dante SOCKS Server
Install Dante for a dedicated SOCKS5 server:
```bash
sudo apt install dante-server
# Configure in /etc/danted.conf
sudo systemctl restart danted
```
## Troubleshooting
### Plugin Not Connecting Through Proxy
1. **Check proxy address format**: Ensure it includes the protocol (http://, socks5://, etc.)
2. **Verify proxy is running**: Test connectivity to the proxy from your Jellyfin server
3. **Check Jellyfin logs**: Look for proxy-related errors in Dashboard → Logs
4. **Firewall rules**: Ensure your firewall allows outbound connections to the proxy
### Authentication Errors
1. Verify username and password are correct
2. Check if your proxy requires domain authentication (DOMAIN\\username)
3. Some proxies may require specific authentication methods not supported by .NET HttpClient
### DNS Resolution
- The plugin resolves domain names before sending requests through the proxy
- If you need DNS resolution through the proxy, you may need to use a VPN or network-level routing instead
## Affected Domains
When proxy is enabled, all requests to these domains will be routed through the proxy:
- `il.srgssr.ch` - SRF Integration Layer API
- `www.srf.ch` - SRF Play v3 API (German)
- `www.rts.ch` - RTS Play v3 API (French)
- `www.rsi.ch` - RSI Play v3 API (Italian)
- `www.rtr.ch` - RTR Play v3 API (Romansh)
- `www.swi.ch` - SWI Play v3 API (International)
## Viewing Logs
To verify the proxy is being used:
1. Go to **Dashboard → Logs**
2. Look for entries containing "Proxy configured"
3. Example log entry:
```
Proxy configured: http://192.168.1.1:8080 (Authentication: False)
```
## Security Notes
- Proxy credentials are stored in Jellyfin's plugin configuration
- Use HTTPS for the proxy connection when possible to encrypt traffic
- Consider using a VPN for more secure routing if dealing with sensitive content
- Regularly update your proxy server and Jellyfin to patch security vulnerabilities
## Alternative: Network-Level Routing
If you prefer network-level routing instead of application proxy, see the `setup-srf-routing.sh` script for IP-based routing tables (requires root access and is more complex).
## Support
If you encounter issues:
1. Check Jellyfin logs for detailed error messages
2. Verify proxy connectivity with `curl --proxy http://proxy:port https://il.srgssr.ch`
3. Open an issue on the GitHub repository with logs and configuration details
+37 -2
View File
@@ -14,6 +14,17 @@ A Jellyfin plugin for accessing SRF Play (Swiss Radio and Television) video-on-d
- Proxy support for routing traffic through alternate gateways - Proxy support for routing traffic through alternate gateways
- Smart caching with reduced TTL for upcoming livestreams - 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.
## Project Status ## Project Status
### ✅ Completed Components ### ✅ Completed Components
@@ -92,16 +103,40 @@ A Jellyfin plugin for accessing SRF Play (Swiss Radio and Television) video-on-d
**Successfully compiling!** All code analysis warnings resolved. **Successfully compiling!** All code analysis warnings resolved.
### 🧪 Testing Status ### 🧪 Testing Status
- [ ] Unit tests - [x] Unit tests (xUnit framework)
- StreamUrlResolver tests
- MetadataCache tests
- More to be added
- [x] API spec validation tests (nightly automated runs)
- All business units (SRF, RTS, RSI, RTR, SWI)
- Response schema validation
- Performance monitoring
- [ ] Integration testing with Jellyfin instance - [ ] Integration testing with Jellyfin instance
- [ ] End-to-end playback testing for VOD content - [ ] End-to-end playback testing for VOD content
- [ ] Live sports streaming validation - [ ] Live sports streaming validation
**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.
### 📝 Next Steps ### 📝 Next Steps
1. Test live sports streaming when events are scheduled 1. Test live sports streaming when events are scheduled
2. Verify Akamai token authentication 2. Verify Akamai token authentication
3. Test with different business units (RTS, RSI, RTR) 3. Test with different business units (RTS, RSI, RTR)
4. Add unit tests 4. Add more unit tests for remaining services
5. Performance optimization if needed 5. Performance optimization if needed
## API Information ## API Information
-57
View File
@@ -1,57 +0,0 @@
#!/bin/bash
# Deploy SRF Play plugin to Jellyfin server
set -e
# Configuration
JELLYFIN_SERVER="192.168.1.4"
JELLYFIN_USER="dtourolle" # Change this to your SSH user
DLL_PATH="Jellyfin.Plugin.SRFPlay/bin/Release/net8.0/Jellyfin.Plugin.SRFPlay.dll"
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${GREEN}=== Deploying SRF Play Plugin ===${NC}\n"
# Check if DLL exists
if [ ! -f "$DLL_PATH" ]; then
echo "Error: DLL not found. Building..."
dotnet build Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj -c Release
fi
echo "Step 1: Copying DLL to Jellyfin server..."
scp "$DLL_PATH" "${JELLYFIN_USER}@${JELLYFIN_SERVER}:~/"
echo -e "\nStep 2: Installing DLL on Jellyfin server..."
ssh "${JELLYFIN_USER}@${JELLYFIN_SERVER}" << 'ENDSSH'
echo "Stopping Jellyfin..."
sudo systemctl stop jellyfin
echo "Backing up old DLL..."
sudo cp /var/lib/jellyfin/plugins/SRF/Jellyfin.Plugin.SRFPlay.dll \
/var/lib/jellyfin/plugins/SRF/Jellyfin.Plugin.SRFPlay.dll.backup || true
echo "Installing new DLL..."
sudo cp ~/Jellyfin.Plugin.SRFPlay.dll /var/lib/jellyfin/plugins/SRF/
sudo chown jellyfin:jellyfin /var/lib/jellyfin/plugins/SRF/Jellyfin.Plugin.SRFPlay.dll
echo "Starting Jellyfin..."
sudo systemctl start jellyfin
echo "Waiting for Jellyfin to start..."
sleep 5
echo "Checking Jellyfin status..."
sudo systemctl status jellyfin --no-pager -l | head -20
ENDSSH
echo -e "\n${GREEN}✓ Deployment complete!${NC}"
echo -e "\n${YELLOW}Next steps:${NC}"
echo "1. Test video playback - GUID errors should be fixed"
echo "2. If videos still don't play due to geo-blocking, run network routing setup:"
echo " scp setup-gateway-routing.sh cleanup-gateway-routing.sh ${JELLYFIN_USER}@${JELLYFIN_SERVER}:~"
echo " ssh ${JELLYFIN_USER}@${JELLYFIN_SERVER}"
echo " chmod +x setup-gateway-routing.sh"
echo " sudo ./setup-gateway-routing.sh"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB