Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a2ceeddee | ||
|
|
c7aad4689e | ||
|
|
c0c37d8987 | ||
|
|
45627ede82 | ||
|
|
8c9bd680a4 | ||
|
|
46e2fbf0f7 | ||
|
|
9ad0c4a393 | ||
|
|
bbfc9e720a | ||
|
|
e58bb9af21 | ||
|
|
aa6d8eae4c | ||
|
|
f1e97b94a7 | ||
|
|
5034941a79 | ||
|
|
0d301df5e8 |
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: ersatztv
|
||||
description: ErsatzTV custom IPTV channel management — REST API, SQLite DB, Jellyfin integration, FFmpeg profiles. Use when managing custom TV channels.
|
||||
---
|
||||
|
||||
# ErsatzTV Channel Management
|
||||
|
||||
Container: `ersatztv` | Port: `8409` | IP: `172.16.238.11` (may change on restart)
|
||||
Web UI: internal only (`http://localhost:8409` via SSH)
|
||||
SQLite DB: `~/downloadswarm/ersatztv/ersatztv.sqlite3` on jazz (owned by root — use `sudo sqlite3`)
|
||||
Image: `ghcr.io/ersatztv/ersatztv:latest` (v26.3.0, repo archived Feb 2026)
|
||||
|
||||
## Architecture
|
||||
|
||||
ErsatzTV uses **MediatR + Blazor** (not REST for mutations). The REST API is limited:
|
||||
- **GET endpoints**: channels, collections, schedules, playouts, shows, movies, artists, ffmpeg profiles, health, search, watermarks
|
||||
- **POST endpoints**: library scan, playout reset, show scan
|
||||
- **No REST CRUD for channels/collections/schedules** — must use SQLite DB directly
|
||||
|
||||
## REST API
|
||||
|
||||
```bash
|
||||
# Via docker exec
|
||||
docker exec ersatztv curl -s http://localhost:8409/api/ENDPOINT
|
||||
```
|
||||
|
||||
### Read Endpoints (GET)
|
||||
```
|
||||
/api/channels # List channels
|
||||
/api/collections # List collections
|
||||
/api/schedules # List schedules
|
||||
/api/playouts # List playouts
|
||||
/api/shows # List shows
|
||||
/api/movies # List movies
|
||||
/api/artists # List artists
|
||||
/api/search # Search items
|
||||
/api/ffmpeg/profiles # FFmpeg profiles
|
||||
/api/watermarks # Watermarks
|
||||
/iptv/channels.m3u # M3U playlist (for Jellyfin)
|
||||
/iptv/xmltv.xml # XMLTV guide data
|
||||
```
|
||||
|
||||
### Mutation Endpoints (POST)
|
||||
```bash
|
||||
# Library scan
|
||||
POST /api/libraries/{id}/scan
|
||||
|
||||
# Scan single show
|
||||
POST /api/libraries/{id}/scan-show \
|
||||
-H "Content-Type: application/json" -d '{"ShowTitle":"Name","DeepScan":false}'
|
||||
|
||||
# Reset channel playout (rebuilds schedule)
|
||||
POST /api/channels/{channelNumber}/playout/reset
|
||||
```
|
||||
|
||||
## SQLite DB Operations
|
||||
|
||||
```bash
|
||||
# Read queries (safe while running, WAL mode)
|
||||
sudo sqlite3 ~/downloadswarm/ersatztv/ersatztv.sqlite3 "QUERY"
|
||||
|
||||
# Write queries — stop container first
|
||||
docker stop ersatztv
|
||||
sudo sqlite3 ~/downloadswarm/ersatztv/ersatztv.sqlite3 "QUERY"
|
||||
docker start ersatztv
|
||||
```
|
||||
|
||||
### Key Queries
|
||||
```sql
|
||||
-- List channels
|
||||
SELECT Id, Number, Name FROM Channel ORDER BY CAST(Number AS INTEGER);
|
||||
|
||||
-- List collections with item counts
|
||||
SELECT c.Id, c.Name, COUNT(ci.Id) as items FROM Collection c LEFT JOIN CollectionItem ci ON ci.CollectionId = c.Id GROUP BY c.Id;
|
||||
|
||||
-- List schedules
|
||||
SELECT Id, Name FROM ProgramSchedule;
|
||||
|
||||
-- Playout (channel-schedule links)
|
||||
SELECT p.Id, c.Number, c.Name, ps.Name as Schedule FROM Playout p JOIN Channel c ON p.ChannelId = c.Id LEFT JOIN ProgramSchedule ps ON p.ProgramScheduleId = ps.Id;
|
||||
|
||||
-- Media counts
|
||||
SELECT 'Shows' as type, COUNT(*) FROM Show UNION ALL SELECT 'Movies', COUNT(*) FROM Movie UNION ALL SELECT 'Episodes', COUNT(*) FROM Episode UNION ALL SELECT 'MusicVideos', COUNT(*) FROM MusicVideo;
|
||||
|
||||
-- Jellyfin source
|
||||
SELECT jms.Id, jc.Address, jms.ServerName FROM JellyfinMediaSource jms JOIN JellyfinConnection jc ON jc.JellyfinMediaSourceId = jms.Id;
|
||||
|
||||
-- Library sync status
|
||||
SELECT l.Id, l.Name, l.MediaKind, jl.ShouldSyncItems FROM Library l JOIN JellyfinLibrary jl ON jl.Id = l.Id;
|
||||
```
|
||||
|
||||
### Channel Setup Workflow (DB)
|
||||
|
||||
**Show-specific channel** (single TV show, shuffled):
|
||||
```sql
|
||||
-- 1. Schedule
|
||||
INSERT INTO ProgramSchedule (Id, FixedStartTimeBehavior, KeepMultiPartEpisodesTogether, Name, RandomStartPoint, ShuffleScheduleItems, TreatCollectionsAsShows)
|
||||
VALUES (<id>, 0, 0, '<name>', 1, 0, 1);
|
||||
-- 2. Schedule item (CollectionType=1 for Show, PlaybackOrder=3 for Shuffle)
|
||||
INSERT INTO ProgramScheduleItem (Id, CollectionType, FillWithGroupMode, GuideMode, "Index", MarathonGroupBy, MarathonShuffleGroups, MarathonShuffleItems, MediaItemId, PlaybackOrder, ProgramScheduleId)
|
||||
VALUES (<id>, 1, 0, 0, 0, 0, 0, 0, <show_id>, 3, <schedule_id>);
|
||||
INSERT INTO ProgramScheduleOneItem (Id) VALUES (<item_id>);
|
||||
-- 3. Channel
|
||||
INSERT INTO Channel (Id, Categories, FFmpegProfileId, FallbackFillerId, "Group", IdleBehavior, IsEnabled, MirrorSourceChannelId, MusicVideoCreditsMode, MusicVideoCreditsTemplate, Name, Number, PlayoutMode, PlayoutOffset, PlayoutSource, PreferredAudioLanguageCode, PreferredAudioTitle, PreferredSubtitleLanguageCode, ShowInEpg, SongVideoMode, SortNumber, StreamSelector, StreamSelectorMode, StreamingMode, SubtitleMode, TranscodeMode, UniqueId, WatermarkId)
|
||||
VALUES (<id>, '', 1, NULL, '<category>', 0, 1, NULL, 0, NULL, '<name>', '<number>', 0, NULL, 0, NULL, NULL, 'eng', 1, 0, <number>.0, NULL, 0, 4, 2, 0, lower(hex(randomblob(4)))||'-'||lower(hex(randomblob(2)))||'-4'||substr(lower(hex(randomblob(2))),2)||'-'||lower(hex(randomblob(2)))||'-'||lower(hex(randomblob(6))), 1);
|
||||
-- 4. Playout
|
||||
INSERT INTO Playout (Id, ChannelId, ProgramScheduleId, ScheduleKind, Seed)
|
||||
VALUES (<id>, <channel_id>, <schedule_id>, 0, abs(random()) % 1000000);
|
||||
```
|
||||
|
||||
**Collection-based channel** (multiple shows, shuffled):
|
||||
```sql
|
||||
-- 1. Collection + items (MediaItemId = Show.Id)
|
||||
INSERT INTO Collection (Id, Name, UseCustomPlaybackOrder) VALUES (<id>, '<name>', 0);
|
||||
INSERT INTO CollectionItem (CollectionId, MediaItemId) VALUES (<coll_id>, <show_id>);
|
||||
-- 2. Schedule (same as above but CollectionType=0, CollectionId set instead of MediaItemId)
|
||||
INSERT INTO ProgramScheduleItem (Id, CollectionId, CollectionType, ..., PlaybackOrder, ProgramScheduleId)
|
||||
VALUES (<id>, <coll_id>, 0, ..., 3, <schedule_id>);
|
||||
-- 3-4. Channel + Playout same as show-specific
|
||||
```
|
||||
|
||||
After creating: `POST /api/channels/{number}/playout/reset`
|
||||
|
||||
## Volume Mounts (matches Jellyfin)
|
||||
|
||||
| Host Path | Container Path |
|
||||
|-----------|---------------|
|
||||
| `~/downloadswarm/ersatztv` | `/config` |
|
||||
| `/mnt/teramind/episodes` | `/data/tvshows` (ro) |
|
||||
| `/mnt/episodes` | `/data/episodes` (ro) |
|
||||
| `/mnt/media/movies` | `/data/movies` (ro) |
|
||||
| `/mnt/media/standup` | `/data/standup` (ro) |
|
||||
| `/mnt/media/music_videos` | `/data/music` (ro) |
|
||||
|
||||
## FFmpeg & Hardware
|
||||
|
||||
- QSV (Intel Quick Sync) hardware acceleration
|
||||
- Resolution: 1920x1080, H264, AAC stereo
|
||||
- Device: `/dev/dri` passed through
|
||||
- HardwareAccelerationKind: 0=None, 1=Qsv, 2=Nvenc, 3=Vaapi, 4=VideoToolbox, 5=Amf
|
||||
|
||||
## Jellyfin Integration
|
||||
|
||||
- Secrets: `/config/jellyfin-secrets.json` (`{"Address":"http://jellyfin:8096","ApiKey":"978033be716d46678a5d3c54ae0e0ff9"}`)
|
||||
- Libraries: Movies(10), TV Shows(11), Music Videos(8), Standup(9)
|
||||
- `JellyfinLibrary.ShouldSyncItems` must be `1` for scans to work
|
||||
|
||||
## Gotchas
|
||||
|
||||
- DB owned by root — always use `sudo sqlite3`
|
||||
- WAL mode: reads OK while running, stop container for writes
|
||||
- No REST API for channel/collection/schedule CRUD — DB scripting only
|
||||
- Secrets file uses PascalCase JSON (`Address`, `ApiKey`)
|
||||
- Scanner is separate binary (`ErsatzTV.Scanner`) — check with `docker top ersatztv | grep Scanner`
|
||||
- EF TPT inheritance: `ProgramScheduleItem` has subtype tables (`ProgramScheduleOneItem`, etc.) — MUST insert into subtype table
|
||||
- External URL logos work for M3U but NOT for watermark burn-in (code checks `File.Exists()`)
|
||||
- `/api/health` returns Blazor HTML, not JSON — use `/api/channels` to verify API
|
||||
- PlaybackOrder enum: 3=Shuffle, 6=SeasonEpisode (use 3 for all channels)
|
||||
- CollectionType enum: 0=Collection, 1=Show (direct show reference via MediaItemId)
|
||||
- SubtitleMode: 0=None, 2=Burn-in. Set to 2 with PreferredSubtitleLanguageCode='eng' for non-music channels
|
||||
- MediaItem.State: 0=Normal, 1=FileNotFound — clean up state=1 items by deleting cascading deps
|
||||
- ProgramSchedule required NOT NULL columns: FixedStartTimeBehavior, KeepMultiPartEpisodesTogether, RandomStartPoint, ShuffleScheduleItems, TreatCollectionsAsShows
|
||||
- Channel required NOT NULL columns: SongVideoMode (set 0), plus all standard columns (see Channel table schema)
|
||||
- After schedule changes, rebuild playout: `POST /api/channels/{number}/playout/reset`
|
||||
- Playout `ScheduleKind` must be `1` (not `0`/None) — `0` causes "Cannot build playout type None" error
|
||||
- M3U `tvg-logo` URLs hardcode `http://localhost:8409` — Jellyfin can't fetch these from inside its container. Fix by downloading logos from ETV and base64-uploading to Jellyfin (see `docs/Docker/ErsatzTV.md` for script). Tracked in issue #171
|
||||
- Repo archived Feb 2026, v26.3.0 is final stable version. Maintainer welcomes forks
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: jellyfin
|
||||
description: Jellyfin media server management — API for libraries, items, streaming, users. Use when managing media library or checking Jellyfin status.
|
||||
---
|
||||
|
||||
# Jellyfin Management
|
||||
|
||||
Container: `jellyfin` | Port: `8096` | IP: `172.16.238.20` (may change on restart)
|
||||
API Token: `978033be716d46678a5d3c54ae0e0ff9`
|
||||
Web UI: `https://jellyfin.tblindustries.be` (NO Authelia — native login, password: `coup1802`)
|
||||
Config: `/home/timothy/downloadswarm/jellyfin/` on jazz
|
||||
|
||||
## Access Pattern
|
||||
|
||||
```bash
|
||||
docker exec jellyfin curl -s 'http://localhost:8096/ENDPOINT' \
|
||||
-H 'X-Emby-Token: 978033be716d46678a5d3c54ae0e0ff9'
|
||||
```
|
||||
|
||||
## Volume Mounts
|
||||
|
||||
| Host Path | Container Path | Content |
|
||||
|-----------|---------------|---------|
|
||||
| `/mnt/teramind/episodes` | `/data/tvshows` | TV shows |
|
||||
| `/mnt/episodes` | `/data/episodes` | More episodes |
|
||||
| `/mnt/media/movies` | `/data/movies` | Movies |
|
||||
| `/mnt/media/standup` | `/data/standup` | Standup |
|
||||
| `/mnt/media/music_videos` | `/data/music` | Music videos |
|
||||
| `/mnt/media/audio/music` | `/data/audio` | Music audio (ro) |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### System
|
||||
```
|
||||
GET /System/Info # Server info, version
|
||||
GET /System/Info/Public # Public info (no auth needed)
|
||||
POST /System/Restart # Restart server
|
||||
```
|
||||
|
||||
### Items (Search & Browse)
|
||||
```bash
|
||||
# Search items
|
||||
GET /Items?includeItemTypes=Movie,Episode,Series&recursive=true&searchTerm=QUERY&fields=Path&limit=20
|
||||
|
||||
# Get item details
|
||||
GET /Items?ids=ITEM_ID&fields=Path,MediaStreams,Overview
|
||||
|
||||
# Get all movies
|
||||
GET /Items?includeItemTypes=Movie&recursive=true&fields=Path&limit=1000
|
||||
|
||||
# Get series
|
||||
GET /Items?includeItemTypes=Series&recursive=true&fields=Path
|
||||
|
||||
# Get episodes for a series
|
||||
GET /Shows/{seriesId}/Episodes?fields=Path,MediaStreams
|
||||
|
||||
# Filter by library (parentId)
|
||||
GET /Items?parentId=LIBRARY_ID&recursive=true&fields=Path
|
||||
```
|
||||
|
||||
### Libraries
|
||||
```
|
||||
GET /Library/VirtualFolders # List all libraries
|
||||
POST /Library/Refresh # Trigger full library scan
|
||||
POST /Items/{id}/Refresh # Refresh single item metadata
|
||||
```
|
||||
|
||||
### Streaming
|
||||
```bash
|
||||
# Test stream URL
|
||||
GET /Videos/{itemId}/stream?static=true
|
||||
|
||||
# Get playback info
|
||||
GET /Items/{itemId}/PlaybackInfo
|
||||
```
|
||||
|
||||
### Users
|
||||
```
|
||||
GET /Users # List users
|
||||
GET /Users/{userId} # User details
|
||||
```
|
||||
|
||||
## Library IDs
|
||||
|
||||
Check with: `curl -s -H "X-Emby-Token: TOKEN" http://localhost:8096/Library/VirtualFolders`
|
||||
|
||||
## Live TV
|
||||
|
||||
- **ErsatzTV** (channels <1000): M3U `http://ersatztv:8409/iptv/channels.m3u`, XMLTV `http://ersatztv:8409/iptv/xmltv.xml`
|
||||
- **Dispatcharr** (channels 1000+): IPTV stream manager on port 9191, separate tuner
|
||||
- Configured in Jellyfin Admin > Live TV
|
||||
- Guide refresh task ID: `bea9b218c97bbf98c5dc1303bdb9a0ca` — trigger via `POST /ScheduledTasks/Running/{id}`
|
||||
- **Logo fix after guide refresh**: ErsatzTV logos break (aspect ratio=0) because M3U uses `localhost:8409`. Fix script in `docs/Docker/ErsatzTV.md` downloads from ETV and base64-uploads to `POST /Items/{id}/Images/Primary` (body = base64, Content-Type = image/png)
|
||||
- **Image upload format**: Jellyfin expects base64-encoded body (NOT raw binary) for `POST /Items/{id}/Images/Primary`
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Passwords**: `coup1802` (NOT `ded89Lm4`) — Jellyfin has native auth, no Authelia
|
||||
- Auth header is `X-Emby-Token` (Jellyfin is an Emby fork)
|
||||
- Music videos are typed as "Movie" in Jellyfin
|
||||
- Music library at `/data/music` maps to `/mnt/media/music_videos` on host (not actual music)
|
||||
- Items return 404 on stream if source volume is unmounted
|
||||
- Jellyfin preserves item IDs across restarts unless files are renamed
|
||||
- Full library scan can take a long time — prefer targeted `/Items/{id}/Refresh`
|
||||
- `ffprobe` available in container for checking media streams: `docker exec jellyfin ffprobe -v quiet -print_format json -show_streams FILE`
|
||||
@@ -0,0 +1,164 @@
|
||||
name: Build ErsatzTV Image
|
||||
|
||||
# Builds the fork's own amd64 image and pushes it to the Gitea container registry.
|
||||
# pull_request -> test job only (no image build/push)
|
||||
# push to main -> :latest + :<short-sha> (test image; does NOT touch prod)
|
||||
# push tag v* -> :prod + :<version> + :<short-sha> (prod release)
|
||||
# workflow_dispatch -> manual run; only publishes when the ref is main or a v* tag
|
||||
#
|
||||
# Runner + registry provisioned in server-management#172. The Gitea registry is
|
||||
# HTTP-only, so BuildKit needs the inline `http = true` config below (it does not
|
||||
# inherit the host daemon's insecure-registries setting).
|
||||
#
|
||||
# `:latest` is intentionally the test/dev channel (per ersatztv#3); prod pins
|
||||
# `:prod`, never `:latest` (enforced in the prod compose — server-management#481).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
# Single runner on jazz: serialize all runs so the push-main-then-tag release
|
||||
# flow can't collide on the shared :buildcache tag or the smoke container.
|
||||
concurrency:
|
||||
group: ersatztv-build
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: 192.168.1.95:3000
|
||||
IMAGE: 192.168.1.95:3000/timothy/ersatztv
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Build & test (.NET)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore
|
||||
|
||||
- name: Strip Scanner project ref (matches Docker build)
|
||||
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --configuration Release --no-build --blame-hang-timeout "2m" --verbosity normal
|
||||
|
||||
build:
|
||||
name: Build & push image (amd64)
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Compute version and tags
|
||||
id: meta
|
||||
run: |
|
||||
SHORT=$(git rev-parse --short HEAD)
|
||||
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
INFO_VERSION="${VERSION}"
|
||||
TAGS=("${IMAGE}:prod" "${IMAGE}:${VERSION}" "${IMAGE}:${SHORT}")
|
||||
else
|
||||
DESC=$(git describe --tags --abbrev=0 2>/dev/null || echo v0.0.0)
|
||||
INFO_VERSION="${DESC#v}-${SHORT}"
|
||||
TAGS=("${IMAGE}:latest" "${IMAGE}:${SHORT}")
|
||||
fi
|
||||
echo "info_version=${INFO_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "short=${SHORT}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "tags<<__EOT__"
|
||||
printf '%s\n' "${TAGS[@]}"
|
||||
echo "__EOT__"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "INFO_VERSION=${INFO_VERSION}"
|
||||
printf 'tag: %s\n' "${TAGS[@]}"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
buildkitd-config-inline: |
|
||||
[registry."192.168.1.95:3000"]
|
||||
http = true
|
||||
|
||||
- name: Login to Gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64
|
||||
# only publish from main or a v* tag; other refs (e.g. branch dispatch) build only
|
||||
push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
|
||||
provenance: false
|
||||
build-args: |
|
||||
INFO_VERSION=${{ steps.meta.outputs.info_version }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=registry,ref=192.168.1.95:3000/timothy/ersatztv:buildcache
|
||||
cache-to: type=registry,ref=192.168.1.95:3000/timothy/ersatztv:buildcache,mode=max,ignore-error=true
|
||||
|
||||
- name: Smoke test (HTTP readiness)
|
||||
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
IMG="${IMAGE}:${{ steps.meta.outputs.short }}"
|
||||
NAME="etv-smoke-${{ github.run_id }}"
|
||||
trap 'docker rm -f "$NAME" >/dev/null 2>&1 || true' EXIT
|
||||
echo "Pulling ${IMG}"
|
||||
docker pull "$IMG"
|
||||
docker run -d --name "$NAME" --memory 2g \
|
||||
-e ETV_CONFIG_FOLDER=/tmp/etv/config \
|
||||
-e ETV_TRANSCODE_FOLDER=/tmp/etv/transcode \
|
||||
"$IMG"
|
||||
# probe ErsatzTV's web server from inside the container (image ships python3)
|
||||
cat > probe.py <<'PY'
|
||||
import urllib.request, urllib.error, sys
|
||||
try:
|
||||
urllib.request.urlopen("http://localhost:8409/", timeout=3)
|
||||
except urllib.error.HTTPError:
|
||||
pass # any HTTP status means the server is serving
|
||||
except Exception:
|
||||
sys.exit(1) # not listening yet
|
||||
PY
|
||||
ok=0
|
||||
for _ in $(seq 1 60); do
|
||||
if [ -z "$(docker ps -q --filter name="$NAME" --filter status=running)" ]; then
|
||||
echo "Container exited early"; break
|
||||
fi
|
||||
if docker exec -i "$NAME" python3 - < probe.py >/dev/null 2>&1; then
|
||||
ok=1; break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "===== container logs (tail) ====="
|
||||
docker logs "$NAME" 2>&1 | tail -n 40 || true
|
||||
echo "================================="
|
||||
if [ "$ok" != "1" ]; then
|
||||
echo "Smoke test FAILED: ErsatzTV did not serve HTTP on :8409"
|
||||
exit 1
|
||||
fi
|
||||
echo "Smoke test passed: ErsatzTV served HTTP on :8409"
|
||||
@@ -1,2 +0,0 @@
|
||||
github: jasongdove
|
||||
custom: "https://www.paypal.me/jasongdove"
|
||||
@@ -1,26 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: nuget
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
assignees:
|
||||
- jasongdove
|
||||
- package-ecosystem: docker
|
||||
directory: "/docker"
|
||||
schedule:
|
||||
interval: daily
|
||||
assignees:
|
||||
- jasongdove
|
||||
- package-ecosystem: docker
|
||||
directory: "/docker/nvidia"
|
||||
schedule:
|
||||
interval: daily
|
||||
assignees:
|
||||
- jasongdove
|
||||
- package-ecosystem: docker
|
||||
directory: "/docker/vaapi"
|
||||
schedule:
|
||||
interval: daily
|
||||
assignees:
|
||||
- jasongdove
|
||||
@@ -1,336 +0,0 @@
|
||||
name: Build Artifacts
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag'
|
||||
required: true
|
||||
type: string
|
||||
release_version:
|
||||
description: 'Release version number (e.g. v0.3.7-alpha)'
|
||||
required: true
|
||||
type: string
|
||||
info_version:
|
||||
description: 'Informational version number (e.g. 0.3.7-alpha)'
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
apple_developer_certificate_p12_base64:
|
||||
required: true
|
||||
apple_developer_certificate_password:
|
||||
required: true
|
||||
ac_username:
|
||||
required: true
|
||||
ac_password:
|
||||
required: true
|
||||
gh_token:
|
||||
required: true
|
||||
azure_client_id:
|
||||
required: true
|
||||
azure_tenant_id:
|
||||
required: true
|
||||
azure_subscription_id:
|
||||
required: true
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
jobs:
|
||||
build_and_upload_mac:
|
||||
name: Mac Build & Upload
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-14
|
||||
kind: macOS
|
||||
target: osx-x64
|
||||
- os: macos-14
|
||||
kind: macOS
|
||||
target: osx-arm64
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore -r "${{ matrix.target}}"
|
||||
|
||||
- name: Import Code-Signing Certificates
|
||||
uses: Apple-Actions/import-codesign-certs@v2
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.apple_developer_certificate_p12_base64 }}
|
||||
p12-password: ${{ secrets.apple_developer_certificate_password }}
|
||||
|
||||
- name: Calculate Release Name
|
||||
shell: bash
|
||||
run: |
|
||||
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
|
||||
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i '' '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net10.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net10.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
|
||||
- name: Bundle
|
||||
shell: bash
|
||||
run: |
|
||||
brew install coreutils
|
||||
plutil -replace CFBundleShortVersionString -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
|
||||
plutil -replace CFBundleVersion -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
|
||||
scripts/macOS/bundle.sh
|
||||
|
||||
- name: Sign
|
||||
shell: bash
|
||||
run: scripts/macOS/sign.sh
|
||||
|
||||
- name: Create DMG
|
||||
shell: bash
|
||||
run: |
|
||||
brew install create-dmg
|
||||
create-dmg \
|
||||
--volname "ErsatzTV" \
|
||||
--volicon "artwork/ErsatzTV.icns" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 800 400 \
|
||||
--icon-size 100 \
|
||||
--icon "ErsatzTV.app" 200 190 \
|
||||
--hide-extension "ErsatzTV.app" \
|
||||
--app-drop-link 600 185 \
|
||||
--skip-jenkins \
|
||||
--no-internet-enable \
|
||||
"ErsatzTV.dmg" \
|
||||
"ErsatzTV.app/"
|
||||
|
||||
- name: Notarize
|
||||
shell: bash
|
||||
run: |
|
||||
xcrun notarytool submit ErsatzTV.dmg --apple-id "${{ secrets.ac_username }}" --password "${{ secrets.ac_password }}" --team-id 32MB98Q32R --wait
|
||||
xcrun stapler staple ErsatzTV.dmg
|
||||
|
||||
- name: Cleanup
|
||||
shell: bash
|
||||
run: |
|
||||
mv ErsatzTV.dmg "${{ env.RELEASE_NAME }}.dmg"
|
||||
rm -r publish
|
||||
rm -r ErsatzTV.app
|
||||
|
||||
- name: Delete old release assets
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
if: ${{ inputs.release_tag == 'develop' }}
|
||||
with:
|
||||
token: ${{ secrets.gh_token }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
fail-if-no-assets: false
|
||||
assets: "*${{ matrix.target }}.dmg"
|
||||
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: false
|
||||
tag_name: ${{ inputs.release_tag }}
|
||||
files: "${{ env.RELEASE_NAME }}.dmg"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.gh_token }}
|
||||
|
||||
build_and_upload_linux:
|
||||
name: Build & Upload Linux
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
kind: linux
|
||||
target: linux-x64
|
||||
- os: ubuntu-latest
|
||||
kind: linux
|
||||
target: linux-musl-x64
|
||||
- os: ubuntu-latest
|
||||
kind: linux
|
||||
target: linux-arm
|
||||
- os: ubuntu-24.04-arm
|
||||
kind: linux
|
||||
target: linux-arm64
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore -r "${{ matrix.target }}"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
# Define some variables for things we need
|
||||
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
|
||||
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
|
||||
|
||||
# Build everything
|
||||
sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net10.0 --runtime "${{ matrix.target }}" -c Release -o "scanner" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net10.0 --runtime "${{ matrix.target }}" -c Release -o "main" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
mkdir "$release_name"
|
||||
mv scanner/* "$release_name/"
|
||||
mv main/* "$release_name/"
|
||||
tar czvf "${release_name}.tar.gz" "$release_name"
|
||||
|
||||
# Delete output directory
|
||||
rm -r "$release_name"
|
||||
|
||||
- name: Delete old release assets
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
if: ${{ inputs.release_tag == 'develop' }}
|
||||
with:
|
||||
token: ${{ secrets.gh_token }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
fail-if-no-assets: false
|
||||
assets: "*${{ matrix.target }}.tar.gz"
|
||||
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: false
|
||||
tag_name: ${{ inputs.release_tag }}
|
||||
files: "${{ env.RELEASE_NAME }}.tar.gz"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.gh_token }}
|
||||
|
||||
build_dotnet_windows:
|
||||
name: Build dotnet for Windows
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore -r "win-x64"
|
||||
|
||||
- name: Build dotnet projects
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net10.0 --runtime "win-x64" -c Release -o "scanner" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-win-x64" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net10.0 --runtime "win-x64" -c Release -o "main" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-win-x64" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
|
||||
- name: Upload .NET Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dotnet-windows-build
|
||||
path: |
|
||||
scanner/
|
||||
main/
|
||||
retention-days: 1
|
||||
|
||||
package_and_upload_windows:
|
||||
name: Package & Upload Windows
|
||||
runs-on: windows-latest
|
||||
needs: build_dotnet_windows
|
||||
steps:
|
||||
- name: Download dotnet artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dotnet-windows-build
|
||||
path: dotnet-build
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.azure_client_id }}
|
||||
tenant-id: ${{ secrets.azure_tenant_id }}
|
||||
subscription-id: ${{ secrets.azure_subscription_id }}
|
||||
enable-AzPSSession: true
|
||||
|
||||
- name: Sign dotnet artifacts
|
||||
uses: azure/trusted-signing-action@v0
|
||||
with:
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
trusted-signing-account-name: ArtifactSigning
|
||||
certificate-profile-name: ErsatzTV
|
||||
files-folder: ${{ github.workspace }}/dotnet-build
|
||||
files-folder-recurse: true
|
||||
files-folder-filter: ErsatzTV.exe,ErsatzTV.Scanner.exe
|
||||
file-digest: SHA256
|
||||
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
||||
timestamp-digest: SHA256
|
||||
|
||||
- name: Download rust launcher
|
||||
uses: suisei-cn/actions-download-file@v1.3.0
|
||||
with:
|
||||
url: "https://github.com/ErsatzTV/ErsatzTV-Windows/releases/download/v1.0.0/ErsatzTV-Windows.exe"
|
||||
target: rust-build/
|
||||
|
||||
- name: Download ffmpeg
|
||||
uses: suisei-cn/actions-download-file@v1.3.0
|
||||
id: downloadffmpeg
|
||||
with:
|
||||
url: "https://github.com/ErsatzTV/ErsatzTV-ffmpeg/releases/download/7.1.1/ffmpeg-n7.1.1-56-gc2184b65d2-win64-gpl-7.1.zip"
|
||||
target: ffmpeg/
|
||||
|
||||
- name: Package artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
release_name="ErsatzTV-${{ inputs.release_version }}-win-x64"
|
||||
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
|
||||
mkdir "$release_name"
|
||||
|
||||
mv dotnet-build/scanner/* "$release_name/"
|
||||
mv dotnet-build/main/* "$release_name/"
|
||||
|
||||
# dotnet shouldn't copy the resources here, but it does
|
||||
rm -rf "$release_name/Resources"
|
||||
|
||||
mv rust-build/ErsatzTV-Windows.exe "$release_name/ErsatzTV-Windows.exe"
|
||||
7z e "ffmpeg/${{ steps.downloadffmpeg.outputs.filename }}" -o"$release_name" '*.exe' -r
|
||||
rm -f "$release_name/ffplay.exe"
|
||||
|
||||
(cd "${release_name}" && 7z a "../${release_name}.zip" .)
|
||||
|
||||
- name: Delete old release assets
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
if: ${{ inputs.release_tag == 'develop' }}
|
||||
with:
|
||||
token: ${{ secrets.gh_token }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
fail-if-no-assets: false
|
||||
assets: "*win-x64.zip"
|
||||
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: false
|
||||
tag_name: ${{ inputs.release_tag }}
|
||||
files: "${{ env.RELEASE_NAME }}.zip"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.gh_token }}
|
||||
@@ -1,62 +0,0 @@
|
||||
name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
calculate_version:
|
||||
name: Calculate version information
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Extract Docker Tag
|
||||
shell: bash
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
tag2="${tag:1}"
|
||||
short=$(git rev-parse --short HEAD)
|
||||
final="${tag2}-${short}"
|
||||
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
||||
- name: Extract Artifacts Version
|
||||
shell: bash
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
short=$(git rev-parse --short HEAD)
|
||||
final="${tag}-${short}"
|
||||
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
|
||||
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
|
||||
outputs:
|
||||
git_tag: ${{ env.GIT_TAG }}
|
||||
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
|
||||
info_version: ${{ env.INFO_VERSION }}
|
||||
build_and_upload:
|
||||
uses: ersatztv/ersatztv/.github/workflows/artifacts.yml@main
|
||||
needs: calculate_version
|
||||
with:
|
||||
release_tag: develop
|
||||
release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
|
||||
info_version: ${{ needs.calculate_version.outputs.info_version }}
|
||||
secrets:
|
||||
apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
|
||||
apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
|
||||
ac_username: ${{ secrets.AC_USERNAME }}
|
||||
ac_password: ${{ secrets.AC_PASSWORD }}
|
||||
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
azure_client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure_tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
build_images:
|
||||
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
|
||||
needs: calculate_version
|
||||
with:
|
||||
base_version: develop
|
||||
info_version: ${{ needs.calculate_version.outputs.git_tag }}
|
||||
tag_version: ${{ github.sha }}
|
||||
secrets:
|
||||
docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
@@ -1,141 +0,0 @@
|
||||
name: Build & Publish to Docker Hub
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
base_version:
|
||||
description: 'Base version (latest or develop)'
|
||||
required: true
|
||||
type: string
|
||||
info_version:
|
||||
description: 'Informational version number (e.g. 0.3.7-alpha)'
|
||||
required: true
|
||||
type: string
|
||||
tag_version:
|
||||
description: 'Docker tag version (e.g. v0.3.7)'
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
docker_hub_username:
|
||||
required: true
|
||||
docker_hub_access_token:
|
||||
required: true
|
||||
jobs:
|
||||
build_images:
|
||||
name: Build ${{ matrix.name }} image
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: contains(github.event.head_commit.message, '[no build]') == false
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: amd64
|
||||
os: ubuntu-latest
|
||||
path: ''
|
||||
suffix: '-amd64'
|
||||
platform: 'linux/amd64'
|
||||
- name: arm32v7
|
||||
os: ubuntu-latest
|
||||
path: 'arm32v7/'
|
||||
suffix: '-arm'
|
||||
platform: 'linux/arm/v7'
|
||||
- name: arm64
|
||||
os: ubuntu-24.04-arm
|
||||
path: 'arm64/'
|
||||
suffix: '-arm64'
|
||||
platform: 'linux/arm64'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
if: ${{ matrix.name == 'arm32v7' }}
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.docker_hub_username }}
|
||||
password: ${{ secrets.docker_hub_access_token }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
provenance: false
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||
outputs: |
|
||||
type=image,name=jasongdove/ersatztv,name-canonical=true,push-by-digest=true
|
||||
type=image,name=ghcr.io/ersatztv/ersatztv,name-canonical=true,push-by-digest=true
|
||||
|
||||
- name: Save digest to artifact
|
||||
run: echo ${{ steps.build.outputs.digest }} > digest.txt
|
||||
|
||||
- name: Upload digest artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digest-${{ matrix.name }}
|
||||
path: digest.txt
|
||||
|
||||
merge_manifests:
|
||||
name: Merge Manifests
|
||||
runs-on: ubuntu-latest
|
||||
needs: build_images
|
||||
steps:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.docker_hub_username }}
|
||||
password: ${{ secrets.docker_hub_access_token }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download all digest artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: digests/
|
||||
|
||||
- name: Read digests from artifacts
|
||||
id: digests
|
||||
run: |
|
||||
AMD64_HASH=$(cat digests/digest-amd64/digest.txt)
|
||||
ARM32V7_HASH=$(cat digests/digest-arm32v7/digest.txt)
|
||||
ARM64_HASH=$(cat digests/digest-arm64/digest.txt)
|
||||
|
||||
DOCKER_HUB_DIGESTS="jasongdove/ersatztv@${AMD64_HASH} jasongdove/ersatztv@${ARM64_HASH} jasongdove/ersatztv@${ARM32V7_HASH}"
|
||||
GHCR_DIGESTS="ghcr.io/ersatztv/ersatztv@${AMD64_HASH} ghcr.io/ersatztv/ersatztv@${ARM64_HASH} ghcr.io/ersatztv/ersatztv@${ARM32V7_HASH}"
|
||||
|
||||
echo "docker_hub_digests=${DOCKER_HUB_DIGESTS}" >> $GITHUB_OUTPUT
|
||||
echo "ghcr_digests=${GHCR_DIGESTS}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and push manifests
|
||||
run: |
|
||||
docker manifest create jasongdove/ersatztv:${{ inputs.base_version }} ${{ steps.digests.outputs.docker_hub_digests }}
|
||||
docker manifest push jasongdove/ersatztv:${{ inputs.base_version }}
|
||||
docker manifest create jasongdove/ersatztv:${{ inputs.tag_version }} ${{ steps.digests.outputs.docker_hub_digests }}
|
||||
docker manifest push jasongdove/ersatztv:${{ inputs.tag_version }}
|
||||
|
||||
docker manifest create ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }} ${{ steps.digests.outputs.ghcr_digests }}
|
||||
docker manifest push ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}
|
||||
docker manifest create ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }} ${{ steps.digests.outputs.ghcr_digests }}
|
||||
docker manifest push ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}
|
||||
@@ -1,27 +0,0 @@
|
||||
name: 'Close stale issues'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
ascending: true
|
||||
days-before-stale: 120
|
||||
days-before-pr-stale: -1
|
||||
days-before-close: 21
|
||||
days-before-pr-close: -1
|
||||
operations-per-run: 500
|
||||
exempt-issue-labels: 'regression,security,roadmap,future,feature,enhancement,confirmed'
|
||||
stale-issue-label: 'stale'
|
||||
stale-issue-message: |-
|
||||
This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
|
||||
|
||||
If you have any questions you can use one of several ways to [contact us](https://ersatztv.org).
|
||||
close-issue-message: |-
|
||||
This issue was closed due to inactivity.
|
||||
@@ -1,118 +0,0 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
pull_request:
|
||||
jobs:
|
||||
build_and_analyze:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Prep project file
|
||||
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore /p:EnableThreadingAnalyzers=true
|
||||
build_and_test_windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Prep project file
|
||||
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal
|
||||
build_and_test_linux:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: linux-x64
|
||||
- os: ubuntu-latest
|
||||
target: linux-musl-x64
|
||||
- os: ubuntu-latest
|
||||
target: linux-arm
|
||||
- os: ubuntu-24.04-arm
|
||||
target: linux-arm64
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore -p:RestoreEnablePackagePruning=true -r "${{ matrix.target }}"
|
||||
|
||||
- name: Prep project file
|
||||
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build ErsatzTV/ErsatzTV.csproj --runtime "${{ matrix.target }}" --configuration Release --no-restore && dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal
|
||||
build_and_test_mac:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Prep project file
|
||||
run: sed -i '' '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal
|
||||
@@ -1,56 +0,0 @@
|
||||
name: Release
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
jobs:
|
||||
calculate_version:
|
||||
name: Calculate version information
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Extract Docker Tag
|
||||
shell: bash
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
||||
echo "DOCKER_TAG=${tag}" >> $GITHUB_ENV
|
||||
- name: Extract Artifacts Version
|
||||
shell: bash
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
echo "ARTIFACTS_VERSION=${tag}" >> $GITHUB_ENV
|
||||
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
|
||||
outputs:
|
||||
git_tag: ${{ env.GIT_TAG }}
|
||||
docker_tag: ${{ env.DOCKER_TAG }}
|
||||
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
|
||||
info_version: ${{ env.INFO_VERSION }}
|
||||
build_and_upload:
|
||||
uses: ersatztv/ersatztv/.github/workflows/artifacts.yml@main
|
||||
needs: calculate_version
|
||||
with:
|
||||
release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
|
||||
release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
|
||||
info_version: ${{ needs.calculate_version.outputs.info_version }}
|
||||
secrets:
|
||||
apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
|
||||
apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
|
||||
ac_username: ${{ secrets.AC_USERNAME }}
|
||||
ac_password: ${{ secrets.AC_PASSWORD }}
|
||||
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
azure_client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure_tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
build_images:
|
||||
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
|
||||
needs: calculate_version
|
||||
with:
|
||||
base_version: latest
|
||||
info_version: ${{ needs.calculate_version.outputs.git_tag }}
|
||||
tag_version: ${{ needs.calculate_version.outputs.docker_tag }}
|
||||
secrets:
|
||||
docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
@@ -3,6 +3,9 @@
|
||||
project.lock.json
|
||||
.DS_Store
|
||||
*.pyc
|
||||
|
||||
# Claude Code
|
||||
.mcp/
|
||||
nupkg/
|
||||
|
||||
# Visual Studio Code
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"docker-mcp": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-server-docker"
|
||||
],
|
||||
"env": {
|
||||
"DOCKER_HOST": "ssh://timothy@192.168.1.99"
|
||||
}
|
||||
},
|
||||
"ssh-mcp": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"ssh-mcp",
|
||||
"--",
|
||||
"--host=192.168.1.99",
|
||||
"--user=timothy"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"gitea": {
|
||||
"command": "gitea-mcp-server",
|
||||
"args": [
|
||||
"-t", "stdio",
|
||||
"-host", "http://192.168.1.95:3000",
|
||||
"-token", "8341af0733ab9ce084ea7adf38b76aa9ebc3bd67"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"csharp-lsp": {
|
||||
"command": "/usr/local/share/dotnet/dotnet",
|
||||
"args": [
|
||||
"run",
|
||||
"--project", "/Users/timothy/ersatztv/.mcp/csharp-lsp-mcp/csharp-lsp-mcp/src/CSharpLspMcp",
|
||||
"-c", "Release"
|
||||
],
|
||||
"env": {
|
||||
"PATH": "/usr/local/share/dotnet:/Users/timothy/.dotnet/tools:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
}
|
||||
},
|
||||
"nuget": {
|
||||
"command": "/usr/local/share/dotnet/dotnet",
|
||||
"args": [
|
||||
"dnx",
|
||||
"NuGet.Mcp.Server",
|
||||
"--source", "https://api.nuget.org/v3/index.json",
|
||||
"--yes"
|
||||
],
|
||||
"env": {
|
||||
"DOTNET_ROOT": "/usr/local/share/dotnet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
### Changed
|
||||
- Remove BugSnag error reporting integration
|
||||
- Remove developer's personal Trakt API key
|
||||
- Users who want to continue to use Trakt must create an API app and set the `Client ID` as the environment variable `TRAKT__CLIENTID`
|
||||
|
||||
### Fixed
|
||||
- Support adding trakt lists using `app.trakt.tv` domain (instead of just `trakt.tv`)
|
||||
|
||||
## [26.3.0] - 2026-02-24
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# ErsatzTV Fork
|
||||
|
||||
Custom IPTV channel server for Jellyfin. Forked from [ErsatzTV/ErsatzTV](https://github.com/ErsatzTV/ErsatzTV) after upstream archival (Feb 2026, v26.3.0). Our fork lives on [Gitea](http://192.168.1.95:3000/timothy/ersatztv).
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Language**: C# / .NET 10, Blazor Server UI (MudBlazor)
|
||||
- **Pattern**: CQRS via MediatR — queries/commands in `ErsatzTV.Application/`
|
||||
- **Database**: EF Core (SQLite default, MySQL optional) — context in `ErsatzTV.Infrastructure/Data/TvContext.cs`
|
||||
- **Media**: FFmpeg via CliWrap, SkiaSharp for logo generation
|
||||
- **Functional C#**: Language Ext (Option, Either monads throughout)
|
||||
|
||||
### Project Layout
|
||||
|
||||
| Project | Role |
|
||||
|---------|------|
|
||||
| `ErsatzTV/` | ASP.NET Core host, Blazor pages, API controllers, DI setup |
|
||||
| `ErsatzTV.Application/` | MediatR handlers (business logic) |
|
||||
| `ErsatzTV.Core/` | Domain entities, interfaces, no infrastructure deps |
|
||||
| `ErsatzTV.Infrastructure/` | EF Core repos, data access |
|
||||
| `ErsatzTV.Infrastructure.Sqlite/` | SQLite-specific implementations |
|
||||
| `ErsatzTV.FFmpeg/` | FFmpeg process wrapper |
|
||||
| `ErsatzTV.Scanner/` | Media library scanning |
|
||||
|
||||
### Key Files
|
||||
|
||||
- **M3U generation**: `ErsatzTV.Core/Iptv/ChannelPlaylist.cs` → `ToM3U()`
|
||||
- **XMLTV generation**: `ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs`
|
||||
- **IPTV controller**: `ErsatzTV/Controllers/IptvController.cs` — `/iptv/*` routes
|
||||
- **Logo generation**: `ErsatzTV.Core/Images/ChannelLogoGenerator.cs`
|
||||
- **Channel entities**: `ErsatzTV.Core/Domain/Channel.cs`
|
||||
- **DB context**: `ErsatzTV.Infrastructure/Data/TvContext.cs`
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Docker host**: jazz (192.168.1.99), container `ersatztv`, port 8409
|
||||
- **Config volume**: `~/downloadswarm/ersatztv/` on jazz → `/config` in container
|
||||
- **SQLite DB**: `/config/ersatztv.sqlite3` (WAL mode, root-owned)
|
||||
- **Images** (our fork, built by `.gitea/workflows/docker-build.yml` → `192.168.1.95:3000/timothy/ersatztv`): push to `main` → `:latest` + `:<sha>` (test image); push `v*` tag → `:prod` + `:<version>` + `:<sha>`. Prod container still runs upstream `ghcr.io/ersatztv/ersatztv:latest` pending cutover (server-management#481). Pipeline details: `docs/ci-cd.md`.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Build
|
||||
dotnet build ErsatzTV.sln
|
||||
|
||||
# Run locally (needs FFmpeg in PATH)
|
||||
dotnet run --project ErsatzTV
|
||||
|
||||
# Docker build
|
||||
docker build -f docker/Dockerfile -t ersatztv:dev .
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- Follow existing MediatR CQRS pattern for new features
|
||||
- Domain logic in `ErsatzTV.Core`, infrastructure in `ErsatzTV.Infrastructure`
|
||||
- Keep Blazor pages thin — delegate to MediatR handlers
|
||||
- Test with xUnit (existing test projects)
|
||||
- **Versioning**: release tags are `vYY.<release-seq>.<patch>` (year · sequential release-within-year · patch) — inherited from upstream, **not** year.month. `v26.3.1` = our infra rebuild of upstream 26.3.0 (no app changes); `v26.4.0` is reserved for the first release with app changes. Never `[skip ci]` a commit you'll tag (it suppresses the release build). Full policy: `docs/ci-cd.md` → Versioning & releases.
|
||||
- Backlog tracked via [Gitea Issues](http://192.168.1.95:3000/timothy/ersatztv/issues)
|
||||
|
||||
## Task Completion Protocol
|
||||
|
||||
Every task that closes a Gitea issue MUST complete ALL of these before it is considered done. Use `/done <issue>` to run through this automatically.
|
||||
|
||||
1. **Root cause** (bug fixes / incidents only): Document WHY the problem existed, not just what was changed. If root cause is unknown, say so explicitly and open a follow-up investigation issue. Fixing symptoms without understanding causes creates recurring problems.
|
||||
2. **Comment on issues** as you work — what you found, what approach you're taking, any deviations from the suggested fix.
|
||||
3. **Push changes**: `git push` all commits before closing. Use `fixes #N` in commit messages to auto-close where appropriate.
|
||||
4. **Close comment**: Add a structured closing comment on the issue covering: what was done, root cause (if applicable), files changed, anything deferred, follow-up issues created, and which docs were updated.
|
||||
5. **Close the issue** via API or `fixes #N` commit. Leave open with a comment only if partially addressed.
|
||||
6. **Update docs**: If the change affects operational behavior, update the relevant Obsidian docs (`~/homelab-docs/`), MEMORY.md, or CLAUDE.md inline — not as a follow-up.
|
||||
7. **Reply to reviewer** (if from adversarial review): Summary of done/deferred/questions. This triggers the next review cycle.
|
||||
|
||||
## Project Boundaries
|
||||
|
||||
**ersatztv OWNS**: ErsatzTV fork code (C#/.NET), channel/collection/schedule management, M3U/XMLTV generation, the ErsatzTV skill in server-management.
|
||||
|
||||
**ersatztv does NOT own**:
|
||||
- Docker compose configs → server-management (`~/downloadswarm/stacks/ersatztv/`)
|
||||
- NFS mounts, Ansible, DNS, networking → server-management
|
||||
- Content sourcing (yt-dlp downloads, Sonarr/Radarr libraries) → media-management (planned)
|
||||
- Jellyfin skill → server-management (symlinked)
|
||||
|
||||
**For infrastructure changes** (Docker, NFS, ports, Authelia): open an issue in `timothy/server-management`.
|
||||
|
||||
**For content/media sourcing questions** (what goes into channels, yt-dlp pipelines): open an issue in `timothy/media-management` once it exists; for now, `timothy/server-management`.
|
||||
|
||||
**For plan/audit reviews**: open `~/adversarial-reviewer` before significant architecture changes.
|
||||
|
||||
**Full cross-project rules**: `~/homelab-docs/Operations/Project Boundaries.md` (https://docs.tblindustries.be).
|
||||
**ErsatzTV docs**: `~/homelab-docs/Docker/ErsatzTV.md` + project-local `docs/` (fork strategy, channels, M3U/XMLTV).
|
||||
@@ -3,5 +3,15 @@
|
||||
<InformationalVersion>develop</InformationalVersion>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
|
||||
<!-- NuGet audit (on by default in .NET 10) reports vulnerable transitive
|
||||
packages as NU1901-1904 warnings. Several projects set
|
||||
TreatWarningsAsErrors=true, which would otherwise fail `dotnet restore`
|
||||
on advisories we can't immediately fix. Demote low/moderate/high audit
|
||||
advisories to warnings (still printed in build logs); NU1904 (critical)
|
||||
stays an error so criticals still block. Track fixes separately.
|
||||
WarningsAsErrors promotes NU1904 in EVERY project (even those without
|
||||
TreatWarningsAsErrors), so "criticals block" actually holds repo-wide. -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);NU1901;NU1902;NU1903</WarningsNotAsErrors>
|
||||
<WarningsAsErrors>$(WarningsAsErrors);NU1904</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bugsnag" Version="4.1.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.10.0" />
|
||||
<PackageReference Include="Humanizer.Core" Version="3.0.1" />
|
||||
<PackageReference Include="MediatR" Version="[12.5.0]" />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
@@ -8,16 +7,13 @@ namespace ErsatzTV.Application.Maintenance;
|
||||
|
||||
public class EmptyTrashHandler : IRequestHandler<EmptyTrash, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public EmptyTrashHandler(
|
||||
IClient client,
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
ISearchIndex searchIndex)
|
||||
{
|
||||
_client = client;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
@@ -27,7 +23,6 @@ public class EmptyTrashHandler : IRequestHandler<EmptyTrash, Either<BaseError, U
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult result = await _searchIndex.Search(
|
||||
_client,
|
||||
"state:FileNotFound",
|
||||
string.Empty,
|
||||
0,
|
||||
|
||||
@@ -110,10 +110,10 @@ public partial class AddTraktListHandler : TraktCommandBase, IRequestHandler<Add
|
||||
return maybeList.Map(_ => Unit.Default);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"https:\/\/trakt\.tv\/users\/([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")]
|
||||
[GeneratedRegex(@"https:\/\/(?:app\.)?trakt\.tv\/users\/([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")]
|
||||
private static partial Regex UriTraktListRegex();
|
||||
|
||||
[GeneratedRegex(@"https:\/\/trakt\.tv\/lists\/([\w\-_]+)\/([\w\-_]+)")]
|
||||
[GeneratedRegex(@"https:\/\/(?:app\.)?trakt\.tv\/lists\/([\w\-_]+)\/([\w\-_]+)")]
|
||||
private static partial Regex UriTraktListRegex2();
|
||||
|
||||
[GeneratedRegex(@"([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using EFCore.BulkExtensions;
|
||||
using ErsatzTV.Application.Channels;
|
||||
using ErsatzTV.Application.Subtitles;
|
||||
@@ -22,7 +21,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
{
|
||||
private readonly IBlockPlayoutBuilder _blockPlayoutBuilder;
|
||||
private readonly IBlockPlayoutFillerBuilder _blockPlayoutFillerBuilder;
|
||||
private readonly IClient _client;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly IExternalJsonPlayoutBuilder _externalJsonPlayoutBuilder;
|
||||
@@ -35,7 +33,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
private readonly IScriptedPlayoutBuilder _scriptedPlayoutBuilder;
|
||||
|
||||
public BuildPlayoutHandler(
|
||||
IClient client,
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IPlayoutBuilder playoutBuilder,
|
||||
IBlockPlayoutBuilder blockPlayoutBuilder,
|
||||
@@ -49,7 +46,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||
ILogger<BuildPlayoutHandler> logger)
|
||||
{
|
||||
_client = client;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_playoutBuilder = playoutBuilder;
|
||||
_blockPlayoutBuilder = blockPlayoutBuilder;
|
||||
@@ -348,7 +344,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
newBuildStatus.Success = false;
|
||||
newBuildStatus.Message = $"Timeout building playout for channel {channelName}";
|
||||
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(
|
||||
$"Timeout building playout for channel {channelName}; this may be a bug!");
|
||||
}
|
||||
@@ -359,7 +354,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
newBuildStatus.Success = false;
|
||||
newBuildStatus.Message = $"Unexpected error building playout for channel {channelName}: {ex}";
|
||||
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(
|
||||
$"Unexpected error building playout for channel {channelName}: {ex.Message}");
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Search;
|
||||
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class QuerySearchIndexAllItemsHandler(IClient client, ISearchIndex searchIndex)
|
||||
public class QuerySearchIndexAllItemsHandler(ISearchIndex searchIndex)
|
||||
: IRequestHandler<QuerySearchIndexAllItems, SearchResultAllItemsViewModel>
|
||||
{
|
||||
public async Task<SearchResultAllItemsViewModel> Handle(
|
||||
@@ -23,7 +22,7 @@ public class QuerySearchIndexAllItemsHandler(IClient client, ISearchIndex search
|
||||
await GetIds(LuceneSearchIndex.RemoteStreamType, request.Query, cancellationToken));
|
||||
|
||||
private async Task<List<int>> GetIds(string type, string query, CancellationToken cancellationToken) =>
|
||||
(await searchIndex.Search(client, $"type:{type} AND ({query})", string.Empty, 0, 0, cancellationToken)).Items
|
||||
(await searchIndex.Search($"type:{type} AND ({query})", string.Empty, 0, 0, cancellationToken)).Items
|
||||
.Map(i => i.Id)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
@@ -9,7 +8,6 @@ using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class QuerySearchIndexArtistsHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<QuerySearchIndexArtists, ArtistCardResultsViewModel>
|
||||
@@ -19,7 +17,6 @@ public class QuerySearchIndexArtistsHandler(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
@@ -17,7 +16,6 @@ namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class
|
||||
QuerySearchIndexEpisodesHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IPlexPathReplacementService plexPathReplacementService,
|
||||
IJellyfinPathReplacementService jellyfinPathReplacementService,
|
||||
@@ -31,7 +29,6 @@ public class
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
@@ -9,7 +8,6 @@ using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class QuerySearchIndexImagesHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<QuerySearchIndexImages, ImageCardResultsViewModel>
|
||||
@@ -19,7 +17,6 @@ public class QuerySearchIndexImagesHandler(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
@@ -10,7 +9,6 @@ using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class QuerySearchIndexMoviesHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: QuerySearchIndexHandlerBase, IRequestHandler<QuerySearchIndexMovies, MovieCardResultsViewModel>
|
||||
@@ -20,7 +18,6 @@ public class QuerySearchIndexMoviesHandler(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
@@ -15,7 +14,6 @@ namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class
|
||||
QuerySearchIndexMusicVideosHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IPlexPathReplacementService plexPathReplacementService,
|
||||
IJellyfinPathReplacementService jellyfinPathReplacementService,
|
||||
@@ -28,7 +26,6 @@ public class
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
@@ -10,7 +9,6 @@ namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class
|
||||
QuerySearchIndexOtherVideosHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<QuerySearchIndexOtherVideos, OtherVideoCardResultsViewModel>
|
||||
@@ -20,7 +18,6 @@ public class
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
@@ -9,7 +8,6 @@ using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class QuerySearchIndexRemoteStreamsHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<QuerySearchIndexRemoteStreams, RemoteStreamCardResultsViewModel>
|
||||
@@ -19,7 +17,6 @@ public class QuerySearchIndexRemoteStreamsHandler(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
@@ -11,7 +10,6 @@ namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class
|
||||
QuerySearchIndexSeasonsHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: QuerySearchIndexHandlerBase, IRequestHandler<QuerySearchIndexSeasons, TelevisionSeasonCardResultsViewModel>
|
||||
@@ -21,7 +19,6 @@ public class
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
@@ -11,7 +10,6 @@ namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class
|
||||
QuerySearchIndexShowsHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: QuerySearchIndexHandlerBase, IRequestHandler<QuerySearchIndexShows, TelevisionShowCardResultsViewModel>
|
||||
@@ -21,7 +19,6 @@ public class
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
@@ -8,7 +7,7 @@ using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class QuerySearchIndexSongsHandler(IClient client, ISearchIndex searchIndex, IDbContextFactory<TvContext> dbContextFactory)
|
||||
public class QuerySearchIndexSongsHandler(ISearchIndex searchIndex, IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<QuerySearchIndexSongs, SongCardResultsViewModel>
|
||||
{
|
||||
public async Task<SongCardResultsViewModel> Handle(
|
||||
@@ -16,7 +15,6 @@ public class QuerySearchIndexSongsHandler(IClient client, ISearchIndex searchInd
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
@@ -11,10 +10,9 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class SearchMoviesHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: SearchUsingSearchIndexHandler(client, searchIndex), IRequestHandler<SearchMovies, List<NamedMediaItemViewModel>>
|
||||
: SearchUsingSearchIndexHandler(searchIndex), IRequestHandler<SearchMovies, List<NamedMediaItemViewModel>>
|
||||
{
|
||||
public async Task<List<NamedMediaItemViewModel>> Handle(SearchMovies request, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
@@ -10,10 +9,9 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class SearchTelevisionSeasonsHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: SearchUsingSearchIndexHandler(client, searchIndex),
|
||||
: SearchUsingSearchIndexHandler(searchIndex),
|
||||
IRequestHandler<SearchTelevisionSeasons, List<NamedMediaItemViewModel>>
|
||||
{
|
||||
public async Task<List<NamedMediaItemViewModel>> Handle(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
@@ -11,10 +10,9 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public class SearchTelevisionShowsHandler(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: SearchUsingSearchIndexHandler(client, searchIndex),
|
||||
: SearchUsingSearchIndexHandler(searchIndex),
|
||||
IRequestHandler<SearchTelevisionShows, List<NamedMediaItemViewModel>>
|
||||
{
|
||||
public async Task<List<NamedMediaItemViewModel>> Handle(
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
using System.Collections.Immutable;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Search;
|
||||
|
||||
namespace ErsatzTV.Application.Search;
|
||||
|
||||
public abstract class SearchUsingSearchIndexHandler(IClient client, ISearchIndex searchIndex)
|
||||
public abstract class SearchUsingSearchIndexHandler(ISearchIndex searchIndex)
|
||||
{
|
||||
private const int PageSize = 10;
|
||||
|
||||
protected async Task<ImmutableHashSet<int>> Search(string type, string query, CancellationToken cancellationToken)
|
||||
{
|
||||
var searchResult = await searchIndex.Search(
|
||||
client,
|
||||
$"type:{type} AND *{query.Replace(" ", @"\ ")}*",
|
||||
string.Empty,
|
||||
0,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.IO.Abstractions;
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application.Channels;
|
||||
using ErsatzTV.Application.Graphics;
|
||||
using ErsatzTV.Application.Maintenance;
|
||||
@@ -22,7 +21,6 @@ namespace ErsatzTV.Application.Streaming;
|
||||
|
||||
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
||||
@@ -42,7 +40,6 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
|
||||
IHlsInitSegmentCache hlsInitSegmentCache,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IMediator mediator,
|
||||
IClient client,
|
||||
IFileSystem fileSystem,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILogger<StartFFmpegSessionHandler> logger,
|
||||
@@ -57,7 +54,6 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
|
||||
_hlsInitSegmentCache = hlsInitSegmentCache;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_mediator = mediator;
|
||||
_client = client;
|
||||
_fileSystem = fileSystem;
|
||||
_localFileSystem = localFileSystem;
|
||||
_logger = logger;
|
||||
@@ -129,7 +125,6 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
|
||||
_ => new HlsSessionWorker(
|
||||
_serviceScopeFactory,
|
||||
_graphicsEngine,
|
||||
_client,
|
||||
OutputFormatKind.Hls,
|
||||
_hlsPlaylistFilter,
|
||||
_hlsInitSegmentCache,
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.IO.Abstractions;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using ErsatzTV.Application.Channels;
|
||||
@@ -28,7 +27,6 @@ namespace ErsatzTV.Application.Streaming;
|
||||
public class HlsSessionWorker : IHlsSessionWorker
|
||||
{
|
||||
private static int _workAheadCount;
|
||||
private readonly IClient _client;
|
||||
private readonly OutputFormatKind _outputFormatKind;
|
||||
private readonly IHlsInitSegmentCache _hlsInitSegmentCache;
|
||||
private readonly Dictionary<long, int> _discontinuityMap = [];
|
||||
@@ -60,7 +58,6 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
public HlsSessionWorker(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IGraphicsEngine graphicsEngine,
|
||||
IClient client,
|
||||
OutputFormatKind outputFormatKind,
|
||||
IHlsPlaylistFilter hlsPlaylistFilter,
|
||||
IHlsInitSegmentCache hlsInitSegmentCache,
|
||||
@@ -73,7 +70,6 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
_serviceScope = serviceScopeFactory.CreateScope();
|
||||
_mediator = _serviceScope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
_graphicsEngine = graphicsEngine;
|
||||
_client = client;
|
||||
_outputFormatKind = outputFormatKind;
|
||||
_hlsInitSegmentCache = hlsInitSegmentCache;
|
||||
_hlsPlaylistFilter = hlsPlaylistFilter;
|
||||
@@ -660,15 +656,6 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
{
|
||||
_logger.LogError(ex, "Error transcoding channel {Channel} - {Message}", _channelNumber, ex.Message);
|
||||
|
||||
try
|
||||
{
|
||||
_client.Notify(ex);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.IO.Abstractions;
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
@@ -13,7 +12,6 @@ using Newtonsoft.Json;
|
||||
namespace ErsatzTV.Application.Streaming;
|
||||
|
||||
public class GetLastPtsTimeHandler(
|
||||
IClient client,
|
||||
IFileSystem fileSystem,
|
||||
ITempFilePool tempFilePool,
|
||||
IConfigElementRepository configElementRepository,
|
||||
@@ -181,9 +179,9 @@ public class GetLastPtsTimeHandler(
|
||||
|
||||
logger.LogWarning("Transcode folder is in bad state; troubleshooting info saved to {File}", file);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
client.Notify(ex);
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bugsnag" Version="4.1.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.10.0" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
@@ -11,7 +9,7 @@ namespace ErsatzTV.Core.Tests.Metadata;
|
||||
public class FallbackMetadataProviderTests
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp() => _fallbackMetadataProvider = new FallbackMetadataProvider(Substitute.For<IClient>());
|
||||
public void SetUp() => _fallbackMetadataProvider = new FallbackMetadataProvider();
|
||||
|
||||
private FallbackMetadataProvider _fallbackMetadataProvider;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Bugsnag;
|
||||
using Dapper;
|
||||
using Destructurama;
|
||||
using ErsatzTV.Core.Domain;
|
||||
@@ -93,8 +92,6 @@ public class ScheduleIntegrationTests
|
||||
services.AddSingleton<ISearchIndex, LuceneSearchIndex>();
|
||||
services.AddSingleton<ILanguageCodeCache, LanguageCodeCache>();
|
||||
|
||||
services.AddSingleton(_ => Substitute.For<IClient>());
|
||||
|
||||
ServiceProvider provider = services.BuildServiceProvider();
|
||||
|
||||
IDbContextFactory<TvContext> factory = provider.GetRequiredService<IDbContextFactory<TvContext>>();
|
||||
@@ -110,7 +107,6 @@ public class ScheduleIntegrationTests
|
||||
await searchIndex.Initialize(
|
||||
new LocalFileSystem(
|
||||
new MockFileSystem(),
|
||||
provider.GetRequiredService<IClient>(),
|
||||
provider.GetRequiredService<ILogger<LocalFileSystem>>()),
|
||||
provider.GetRequiredService<IConfigElementRepository>(),
|
||||
_cancellationToken);
|
||||
@@ -123,7 +119,7 @@ public class ScheduleIntegrationTests
|
||||
|
||||
var builder = new PlayoutBuilder(
|
||||
new ConfigElementRepository(factory),
|
||||
new MediaCollectionRepository(Substitute.For<IClient>(), searchIndex, factory),
|
||||
new MediaCollectionRepository(searchIndex, factory),
|
||||
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
|
||||
new ArtistRepository(factory),
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(),
|
||||
@@ -319,7 +315,7 @@ public class ScheduleIntegrationTests
|
||||
|
||||
var builder = new PlayoutBuilder(
|
||||
new ConfigElementRepository(factory),
|
||||
new MediaCollectionRepository(Substitute.For<IClient>(), Substitute.For<ISearchIndex>(), factory),
|
||||
new MediaCollectionRepository(Substitute.For<ISearchIndex>(), factory),
|
||||
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
|
||||
new ArtistRepository(factory),
|
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(),
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bugsnag" Version="4.1.0" />
|
||||
<PackageReference Include="Destructurama.Attributed" Version="5.2.0" />
|
||||
<PackageReference Include="Flurl" Version="4.0.0" />
|
||||
<PackageReference Include="Humanizer.Core" Version="3.0.1" />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
@@ -12,7 +11,6 @@ namespace ErsatzTV.Core.FFmpeg;
|
||||
|
||||
public class FFmpegProcessService
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
|
||||
private readonly ILogger<FFmpegProcessService> _logger;
|
||||
private readonly ITempFilePool _tempFilePool;
|
||||
@@ -20,12 +18,10 @@ public class FFmpegProcessService
|
||||
public FFmpegProcessService(
|
||||
IFFmpegStreamSelector ffmpegStreamSelector,
|
||||
ITempFilePool tempFilePool,
|
||||
IClient client,
|
||||
ILogger<FFmpegProcessService> logger)
|
||||
{
|
||||
_ffmpegStreamSelector = ffmpegStreamSelector;
|
||||
_tempFilePool = tempFilePool;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -131,7 +127,6 @@ public class FFmpegProcessService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error generating song image");
|
||||
_client.Notify(ex);
|
||||
return Left(BaseError.New(ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace ErsatzTV.Core.Health.Checks;
|
||||
|
||||
public interface IErrorReportsHealthCheck : IHealthCheck
|
||||
{
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Search;
|
||||
@@ -38,7 +37,6 @@ public interface ISearchIndex : IDisposable
|
||||
Task<bool> RemoveItems(IEnumerable<int> ids);
|
||||
|
||||
Task<SearchResult> Search(
|
||||
IClient client,
|
||||
string query,
|
||||
string smartCollectionName,
|
||||
int skip,
|
||||
@@ -46,7 +44,6 @@ public interface ISearchIndex : IDisposable
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<SearchResult> Search(
|
||||
IClient client,
|
||||
string query,
|
||||
string smartCollectionName,
|
||||
int skip,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata;
|
||||
|
||||
public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadataProvider
|
||||
public partial class FallbackMetadataProvider : IFallbackMetadataProvider
|
||||
{
|
||||
private static readonly Regex SeasonPattern = SeasonNumber();
|
||||
|
||||
@@ -203,7 +202,7 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
|
||||
[GeneratedRegex(@"s(?:eason)?\s?(\d+)(?![e\d])", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex SeasonNumber();
|
||||
|
||||
private List<EpisodeMetadata> GetEpisodeMetadata(string fileName, EpisodeMetadata baseMetadata)
|
||||
private static List<EpisodeMetadata> GetEpisodeMetadata(string fileName, EpisodeMetadata baseMetadata)
|
||||
{
|
||||
var result = new List<EpisodeMetadata> { baseMetadata };
|
||||
|
||||
@@ -260,15 +259,15 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
client.Notify(ex);
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private MovieMetadata GetMovieMetadata(string fileName, MovieMetadata metadata)
|
||||
private static MovieMetadata GetMovieMetadata(string fileName, MovieMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -283,15 +282,15 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
client.Notify(ex);
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Option<MusicVideoMetadata> GetMusicVideoMetadata(string fileName, MusicVideoMetadata metadata)
|
||||
private static Option<MusicVideoMetadata> GetMusicVideoMetadata(string fileName, MusicVideoMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -310,14 +309,13 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
|
||||
|
||||
return metadata;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private Option<OtherVideoMetadata> GetOtherVideoMetadata(string path, OtherVideoMetadata metadata)
|
||||
private static Option<OtherVideoMetadata> GetOtherVideoMetadata(string path, OtherVideoMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -348,14 +346,13 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
|
||||
|
||||
return metadata;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private Option<ImageMetadata> GetImageMetadata(string path, ImageMetadata metadata)
|
||||
private static Option<ImageMetadata> GetImageMetadata(string path, ImageMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -386,14 +383,13 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
|
||||
|
||||
return metadata;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private Option<RemoteStreamMetadata> GetRemoteStreamMetadata(string path, RemoteStreamMetadata metadata)
|
||||
private static Option<RemoteStreamMetadata> GetRemoteStreamMetadata(string path, RemoteStreamMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -424,14 +420,13 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
|
||||
|
||||
return metadata;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private Option<SongMetadata> GetSongMetadata(string path, SongMetadata metadata)
|
||||
private static Option<SongMetadata> GetSongMetadata(string path, SongMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -462,14 +457,13 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
|
||||
|
||||
return metadata;
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private ShowMetadata GetTelevisionShowMetadata(string fileName, ShowMetadata metadata)
|
||||
private static ShowMetadata GetTelevisionShowMetadata(string fileName, ShowMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -484,9 +478,9 @@ public partial class FallbackMetadataProvider(IClient client) : IFallbackMetadat
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
client.Notify(ex);
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return metadata;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO.Abstractions;
|
||||
using System.Security.Cryptography;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata;
|
||||
|
||||
public class LocalFileSystem(IFileSystem fileSystem, IClient client, ILogger<LocalFileSystem> logger) : ILocalFileSystem
|
||||
public class LocalFileSystem(IFileSystem fileSystem, ILogger<LocalFileSystem> logger) : ILocalFileSystem
|
||||
{
|
||||
public Unit EnsureFolderExists(string folder)
|
||||
{
|
||||
@@ -50,10 +49,9 @@ public class LocalFileSystem(IFileSystem fileSystem, IClient client, ILogger<Loc
|
||||
{
|
||||
logger.LogWarning("Unauthorized access exception listing subdirectories of folder {Folder}", folder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
client.Notify(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +71,9 @@ public class LocalFileSystem(IFileSystem fileSystem, IClient client, ILogger<Loc
|
||||
{
|
||||
logger.LogWarning("Unauthorized access exception listing files in folder {Folder}", folder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
client.Notify(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +93,9 @@ public class LocalFileSystem(IFileSystem fileSystem, IClient client, ILogger<Loc
|
||||
{
|
||||
logger.LogWarning("Unauthorized access exception listing files in folder {Folder}", folder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
client.Notify(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,10 +119,9 @@ public class LocalFileSystem(IFileSystem fileSystem, IClient client, ILogger<Loc
|
||||
{
|
||||
logger.LogWarning("Unauthorized access exception listing files in folder {Folder}", folder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
client.Notify(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +146,6 @@ public class LocalFileSystem(IFileSystem fileSystem, IClient client, ILogger<Loc
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.FFmpeg.Capabilities;
|
||||
@@ -24,7 +23,6 @@ public class LocalStatisticsProviderTests
|
||||
Substitute.For<IMetadataRepository>(),
|
||||
new MockFileSystem(),
|
||||
Substitute.For<ILocalFileSystem>(),
|
||||
Substitute.For<IClient>(),
|
||||
Substitute.For<IHardwareCapabilitiesFactory>(),
|
||||
Substitute.For<ILogger<LocalStatisticsProvider>>());
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Bugsnag;
|
||||
using Dapper;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,16 +13,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories;
|
||||
|
||||
public class MediaCollectionRepository : IMediaCollectionRepository
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public MediaCollectionRepository(
|
||||
IClient client,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_client = client;
|
||||
_searchIndex = searchIndex;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
@@ -463,7 +459,6 @@ public class MediaCollectionRepository : IMediaCollectionRepository
|
||||
|
||||
// elasticsearch doesn't like when we ask for a limit of zero, so use 10,000
|
||||
SearchResult searchResults = await _searchIndex.Search(
|
||||
_client,
|
||||
query,
|
||||
smartCollectionName,
|
||||
0,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Health;
|
||||
using ErsatzTV.Core.Health.Checks;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Health.Checks;
|
||||
|
||||
public class ErrorReportsHealthCheck : BaseHealthCheck, IErrorReportsHealthCheck
|
||||
{
|
||||
private readonly IOptions<BugsnagConfiguration> _bugsnagConfiguration;
|
||||
|
||||
public ErrorReportsHealthCheck(IOptions<BugsnagConfiguration> bugsnagConfiguration) =>
|
||||
_bugsnagConfiguration = bugsnagConfiguration;
|
||||
|
||||
public override string Title => "Error Reports";
|
||||
|
||||
public Task<HealthCheckResult> Check(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_bugsnagConfiguration.Value.Enable)
|
||||
{
|
||||
return Result(
|
||||
HealthCheckStatus.Pass,
|
||||
"Automated error reporting is enabled, thank you! To disable, edit the file appsettings.json or set the Bugsnag:Enable environment variable to false",
|
||||
"Automated error reporting is enabled, thank you!")
|
||||
.AsTask();
|
||||
}
|
||||
|
||||
return InfoResult(
|
||||
"Automated error reporting is disabled. Please enable to support bug fixing efforts!",
|
||||
"Automated error reporting is disabled")
|
||||
.AsTask();
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ public class HealthCheckService : IHealthCheckService
|
||||
IFileNotFoundHealthCheck fileNotFoundHealthCheck,
|
||||
IUnavailableHealthCheck unavailableHealthCheck,
|
||||
IVaapiDriverHealthCheck vaapiDriverHealthCheck,
|
||||
IErrorReportsHealthCheck errorReportsHealthCheck,
|
||||
IUnifiedDockerHealthCheck unifiedDockerHealthCheck,
|
||||
IDowngradeHealthCheck downgradeHealthCheck,
|
||||
IEmptyScheduleHealthCheck emptyScheduleHealthCheck,
|
||||
@@ -53,8 +52,7 @@ public class HealthCheckService : IHealthCheckService
|
||||
fileNotFoundHealthCheck,
|
||||
unavailableHealthCheck,
|
||||
emptyScheduleHealthCheck,
|
||||
vaapiDriverHealthCheck,
|
||||
errorReportsHealthCheck
|
||||
vaapiDriverHealthCheck
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Globalization;
|
||||
using System.IO.Abstractions;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using ErsatzTV.Core;
|
||||
@@ -21,7 +20,6 @@ namespace ErsatzTV.Infrastructure.Metadata;
|
||||
|
||||
public partial class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<LocalStatisticsProvider> _logger;
|
||||
@@ -32,14 +30,12 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
IMetadataRepository metadataRepository,
|
||||
IFileSystem fileSystem,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IClient client,
|
||||
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory,
|
||||
ILogger<LocalStatisticsProvider> logger)
|
||||
{
|
||||
_metadataRepository = metadataRepository;
|
||||
_fileSystem = fileSystem;
|
||||
_localFileSystem = localFileSystem;
|
||||
_client = client;
|
||||
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -71,7 +67,6 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to refresh statistics for media item {Id}", mediaItem.Id);
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -130,7 +125,6 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to get format tags for media item {Id}", mediaItem.Id);
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -225,7 +219,6 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to refresh statistics for media item {Id}", mediaItem.Id);
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -466,9 +459,8 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
_logger.LogError("Duration analysis failed for media item at {Path}", path);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
_logger.LogError("Duration analysis failed for media item at {Path}", path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Bugsnag;
|
||||
using Elastic.Clients.Elasticsearch.Aggregations;
|
||||
using Elastic.Clients.Elasticsearch.Core.Bulk;
|
||||
using Elastic.Clients.Elasticsearch.IndexManagement;
|
||||
@@ -168,13 +167,11 @@ public class ElasticSearchIndex : ISearchIndex
|
||||
}
|
||||
|
||||
public Task<SearchResult> Search(
|
||||
IClient client,
|
||||
string query,
|
||||
string smartCollectionName,
|
||||
int skip,
|
||||
int limit,
|
||||
CancellationToken cancellationToken) => Search(
|
||||
client,
|
||||
query,
|
||||
smartCollectionName,
|
||||
skip,
|
||||
@@ -183,7 +180,6 @@ public class ElasticSearchIndex : ISearchIndex
|
||||
cancellationToken);
|
||||
|
||||
public async Task<SearchResult> Search(
|
||||
IClient client,
|
||||
string query,
|
||||
string smartCollectionName,
|
||||
int skip,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
@@ -212,13 +211,11 @@ public sealed class LuceneSearchIndex : ISearchIndex
|
||||
|
||||
// default to title field only
|
||||
public Task<SearchResult> Search(
|
||||
IClient client,
|
||||
string query,
|
||||
string smartCollectionName,
|
||||
int skip,
|
||||
int limit,
|
||||
CancellationToken cancellationToken) => Search(
|
||||
client,
|
||||
query,
|
||||
smartCollectionName,
|
||||
skip,
|
||||
@@ -227,7 +224,6 @@ public sealed class LuceneSearchIndex : ISearchIndex
|
||||
cancellationToken);
|
||||
|
||||
public async Task<SearchResult> Search(
|
||||
IClient client,
|
||||
string query,
|
||||
string smartCollectionName,
|
||||
int skip,
|
||||
@@ -235,15 +231,6 @@ public sealed class LuceneSearchIndex : ISearchIndex
|
||||
List<string> defaultFields,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "searchQuery", query },
|
||||
{ "skip", skip.ToString(CultureInfo.InvariantCulture) },
|
||||
{ "limit", limit.ToString(CultureInfo.InvariantCulture) }
|
||||
};
|
||||
|
||||
client?.Breadcrumbs?.Leave("SearchIndex.Search", BreadcrumbType.State, metadata);
|
||||
|
||||
query ??= string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query.Replace("*", string.Empty).Replace("?", string.Empty)) ||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using ErsatzTV.Core;
|
||||
@@ -246,7 +245,6 @@ public class TranscodingTests
|
||||
{
|
||||
var localFileSystem = new LocalFileSystem(
|
||||
new RealFileSystem(),
|
||||
Substitute.For<IClient>(),
|
||||
LoggerFactory.CreateLogger<LocalFileSystem>());
|
||||
var fileSystem = new MockFileSystem();
|
||||
var tempFilePool = new TempFilePool();
|
||||
@@ -276,7 +274,6 @@ public class TranscodingTests
|
||||
var oldService = new FFmpegProcessService(
|
||||
new FakeStreamSelector(),
|
||||
tempFilePool,
|
||||
Substitute.For<IClient>(),
|
||||
LoggerFactory.CreateLogger<FFmpegProcessService>());
|
||||
|
||||
var service = new FFmpegLibraryProcessService(
|
||||
@@ -368,7 +365,6 @@ public class TranscodingTests
|
||||
metadataRepository,
|
||||
fileSystem,
|
||||
localFileSystem,
|
||||
Substitute.For<IClient>(),
|
||||
Substitute.For<IHardwareCapabilitiesFactory>(),
|
||||
LoggerFactory.CreateLogger<LocalStatisticsProvider>());
|
||||
|
||||
@@ -484,7 +480,6 @@ public class TranscodingTests
|
||||
|
||||
var localFileSystem = new LocalFileSystem(
|
||||
new MockFileSystem(),
|
||||
Substitute.For<IClient>(),
|
||||
LoggerFactory.CreateLogger<LocalFileSystem>());
|
||||
var fileSystem = new MockFileSystem();
|
||||
|
||||
@@ -533,7 +528,6 @@ public class TranscodingTests
|
||||
metadataRepository,
|
||||
fileSystem,
|
||||
localFileSystem,
|
||||
Substitute.For<IClient>(),
|
||||
Substitute.For<IHardwareCapabilitiesFactory>(),
|
||||
LoggerFactory.CreateLogger<LocalStatisticsProvider>());
|
||||
|
||||
@@ -1007,7 +1001,6 @@ public class TranscodingTests
|
||||
var oldService = new FFmpegProcessService(
|
||||
new FakeStreamSelector(),
|
||||
Substitute.For<ITempFilePool>(),
|
||||
Substitute.For<IClient>(),
|
||||
LoggerFactory.CreateLogger<FFmpegProcessService>());
|
||||
|
||||
var service = new FFmpegLibraryProcessService(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
@@ -61,7 +60,7 @@ public class LocalSubtitlesProviderTests
|
||||
Substitute.For<IMediaItemRepository>(),
|
||||
Substitute.For<IMetadataRepository>(),
|
||||
fileSystem,
|
||||
new LocalFileSystem(fileSystem, Substitute.For<IClient>(), Substitute.For<ILogger<LocalFileSystem>>()),
|
||||
new LocalFileSystem(fileSystem, Substitute.For<ILogger<LocalFileSystem>>()),
|
||||
Substitute.For<ILogger<LocalSubtitlesProvider>>());
|
||||
|
||||
List<Subtitle> result = provider.LocateExternalSubtitles(
|
||||
@@ -115,7 +114,7 @@ public class LocalSubtitlesProviderTests
|
||||
Substitute.For<IMediaItemRepository>(),
|
||||
Substitute.For<IMetadataRepository>(),
|
||||
fileSystem,
|
||||
new LocalFileSystem(fileSystem, Substitute.For<IClient>(), Substitute.For<ILogger<LocalFileSystem>>()),
|
||||
new LocalFileSystem(fileSystem, Substitute.For<ILogger<LocalFileSystem>>()),
|
||||
Substitute.For<ILogger<LocalSubtitlesProvider>>());
|
||||
|
||||
List<Subtitle> result = provider.LocateExternalSubtitles(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
@@ -1032,7 +1031,7 @@ public class MovieFolderScannerTests
|
||||
return new MovieFolderScanner(
|
||||
_scannerProxy,
|
||||
fileSystem,
|
||||
new LocalFileSystem(fileSystem, Substitute.For<IClient>(), Substitute.For<ILogger<LocalFileSystem>>()),
|
||||
new LocalFileSystem(fileSystem, Substitute.For<ILogger<LocalFileSystem>>()),
|
||||
_movieRepository,
|
||||
_localStatisticsProvider,
|
||||
Substitute.For<ILocalSubtitlesProvider>(),
|
||||
@@ -1044,7 +1043,6 @@ public class MovieFolderScannerTests
|
||||
_mediaItemRepository,
|
||||
Substitute.For<IFFmpegPngService>(),
|
||||
Substitute.For<ITempFilePool>(),
|
||||
Substitute.For<IClient>(),
|
||||
Logger);
|
||||
}
|
||||
|
||||
@@ -1060,7 +1058,7 @@ public class MovieFolderScannerTests
|
||||
return new MovieFolderScanner(
|
||||
_scannerProxy,
|
||||
fileSystem,
|
||||
new LocalFileSystem(fileSystem, Substitute.For<IClient>(), Substitute.For<ILogger<LocalFileSystem>>()),
|
||||
new LocalFileSystem(fileSystem, Substitute.For<ILogger<LocalFileSystem>>()),
|
||||
_movieRepository,
|
||||
_localStatisticsProvider,
|
||||
Substitute.For<ILocalSubtitlesProvider>(),
|
||||
@@ -1072,7 +1070,6 @@ public class MovieFolderScannerTests
|
||||
_mediaItemRepository,
|
||||
Substitute.For<IFFmpegPngService>(),
|
||||
Substitute.For<ITempFilePool>(),
|
||||
Substitute.For<IClient>(),
|
||||
Logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IO;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Serilog;
|
||||
using Shouldly;
|
||||
@@ -17,7 +15,6 @@ public class ArtistNfoReaderTests
|
||||
[SetUp]
|
||||
public void SetUp() => _artistNfoReader = new ArtistNfoReader(
|
||||
new RecyclableMemoryStreamManager(),
|
||||
Substitute.For<IClient>(),
|
||||
_logger);
|
||||
|
||||
private readonly ILogger<ArtistNfoReader> _logger;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IO;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Serilog;
|
||||
using Shouldly;
|
||||
@@ -17,7 +15,6 @@ public class EpisodeNfoReaderTests
|
||||
[SetUp]
|
||||
public void SetUp() => _episodeNfoReader = new EpisodeNfoReader(
|
||||
new RecyclableMemoryStreamManager(),
|
||||
Substitute.For<IClient>(),
|
||||
_logger);
|
||||
|
||||
private readonly ILogger<EpisodeNfoReader> _logger;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.IO;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
@@ -16,7 +14,6 @@ public class MovieNfoReaderTests
|
||||
[SetUp]
|
||||
public void SetUp() => _movieNfoReader = new MovieNfoReader(
|
||||
new RecyclableMemoryStreamManager(),
|
||||
Substitute.For<IClient>(),
|
||||
new NullLogger<MovieNfoReader>());
|
||||
|
||||
private MovieNfoReader _movieNfoReader;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.IO;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
@@ -16,7 +14,6 @@ public class MusicVideoNfoReaderTests
|
||||
[SetUp]
|
||||
public void SetUp() => _musicVideoNfoReader = new MusicVideoNfoReader(
|
||||
new RecyclableMemoryStreamManager(),
|
||||
Substitute.For<IClient>(),
|
||||
new NullLogger<MusicVideoNfoReader>());
|
||||
|
||||
private MusicVideoNfoReader _musicVideoNfoReader;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.IO;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
@@ -16,7 +14,6 @@ public class OtherVideoNfoReaderTests
|
||||
[SetUp]
|
||||
public void SetUp() => _otherVideoNfoReader = new OtherVideoNfoReader(
|
||||
new RecyclableMemoryStreamManager(),
|
||||
Substitute.For<IClient>(),
|
||||
new NullLogger<OtherVideoNfoReader>());
|
||||
|
||||
private OtherVideoNfoReader _otherVideoNfoReader;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.IO;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
@@ -16,7 +14,6 @@ public class ShowNfoReaderTests
|
||||
[SetUp]
|
||||
public void SetUp() => _showNfoReader = new ShowNfoReader(
|
||||
new RecyclableMemoryStreamManager(),
|
||||
Substitute.For<IClient>(),
|
||||
new NullLogger<ShowNfoReader>());
|
||||
|
||||
private ShowNfoReader _showNfoReader;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
@@ -19,7 +18,6 @@ namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly IImageRepository _imageRepository;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly IScannerProxy _scannerProxy;
|
||||
@@ -42,7 +40,6 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IFFmpegPngService ffmpegPngService,
|
||||
ITempFilePool tempFilePool,
|
||||
IClient client,
|
||||
ILogger<ImageFolderScanner> logger) : base(
|
||||
fileSystem,
|
||||
localStatisticsProvider,
|
||||
@@ -51,7 +48,6 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
|
||||
imageCache,
|
||||
ffmpegPngService,
|
||||
tempFilePool,
|
||||
client,
|
||||
logger)
|
||||
{
|
||||
_scannerProxy = scannerProxy;
|
||||
@@ -61,7 +57,6 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
|
||||
_imageRepository = imageRepository;
|
||||
_libraryRepository = libraryRepository;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -310,7 +305,6 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
@@ -55,7 +54,6 @@ public abstract class LocalFolderScanner
|
||||
"yml"
|
||||
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly IClient _client;
|
||||
private readonly IFFmpegPngService _ffmpegPngService;
|
||||
|
||||
private readonly IImageCache _imageCache;
|
||||
@@ -75,7 +73,6 @@ public abstract class LocalFolderScanner
|
||||
IImageCache imageCache,
|
||||
IFFmpegPngService ffmpegPngService,
|
||||
ITempFilePool tempFilePool,
|
||||
IClient client,
|
||||
ILogger logger)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
@@ -85,7 +82,6 @@ public abstract class LocalFolderScanner
|
||||
_imageCache = imageCache;
|
||||
_ffmpegPngService = ffmpegPngService;
|
||||
_tempFilePool = tempFilePool;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -129,7 +125,6 @@ public abstract class LocalFolderScanner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -278,7 +273,6 @@ public abstract class LocalFolderScanner
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error refreshing artwork");
|
||||
_client.Notify(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +298,6 @@ public abstract class LocalFolderScanner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
@@ -19,7 +18,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
{
|
||||
private readonly IArtistNfoReader _artistNfoReader;
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly IClient _client;
|
||||
private readonly IEpisodeNfoReader _episodeNfoReader;
|
||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
@@ -57,7 +55,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
IShowNfoReader showNfoReader,
|
||||
IOtherVideoNfoReader otherVideoNfoReader,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
IClient client,
|
||||
ILogger<LocalMetadataProvider> logger)
|
||||
{
|
||||
_metadataRepository = metadataRepository;
|
||||
@@ -78,7 +75,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
_showNfoReader = showNfoReader;
|
||||
_otherVideoNfoReader = otherVideoNfoReader;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -345,7 +341,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read music video nfo metadata from {Path}", nfoFileName);
|
||||
_client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -430,7 +425,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read embedded song metadata from {Path}", path);
|
||||
_client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -498,7 +492,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read embedded song metadata from {Path}", path);
|
||||
_client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -554,7 +547,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read embedded remote stream metadata from {Path}", path);
|
||||
_client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -1327,7 +1319,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read TV show nfo metadata from {Path}", nfoFileName);
|
||||
_client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -1367,7 +1358,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read artist nfo metadata from {Path}", nfoFileName);
|
||||
_client.Notify(ex);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -1422,7 +1412,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read TV episode nfo metadata from {Path}", nfoFileName);
|
||||
_client.Notify(ex);
|
||||
return _fallbackMetadataProvider.GetFallbackMetadata(episode);
|
||||
}
|
||||
}
|
||||
@@ -1509,7 +1498,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read Movie nfo metadata from {Path}", nfoFileName);
|
||||
_client.Notify(ex);
|
||||
return _fallbackMetadataProvider.GetFallbackMetadata(movie);
|
||||
}
|
||||
}
|
||||
@@ -1583,7 +1571,6 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read OtherVideo nfo metadata from {Path}", nfoFileName);
|
||||
_client.Notify(ex);
|
||||
}
|
||||
|
||||
return None;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
@@ -20,7 +19,6 @@ namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILocalChaptersProvider _localChaptersProvider;
|
||||
private readonly IScannerProxy _scannerProxy;
|
||||
@@ -48,7 +46,6 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IFFmpegPngService ffmpegPngService,
|
||||
ITempFilePool tempFilePool,
|
||||
IClient client,
|
||||
ILogger<MovieFolderScanner> logger)
|
||||
: base(
|
||||
fileSystem,
|
||||
@@ -58,7 +55,6 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
||||
imageCache,
|
||||
ffmpegPngService,
|
||||
tempFilePool,
|
||||
client,
|
||||
logger)
|
||||
{
|
||||
_scannerProxy = scannerProxy;
|
||||
@@ -71,7 +67,6 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
||||
_metadataRepository = metadataRepository;
|
||||
_libraryRepository = libraryRepository;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -301,7 +296,6 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -332,7 +326,6 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -348,7 +341,6 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -364,7 +356,6 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
@@ -20,7 +19,6 @@ namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScanner
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly IClient _client;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILocalChaptersProvider _localChaptersProvider;
|
||||
private readonly IMetadataRepository _metadataRepository;
|
||||
@@ -49,7 +47,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IFFmpegPngService ffmpegPngService,
|
||||
ITempFilePool tempFilePool,
|
||||
IClient client,
|
||||
ILogger<MusicVideoFolderScanner> logger) : base(
|
||||
fileSystem,
|
||||
localStatisticsProvider,
|
||||
@@ -58,7 +55,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
||||
imageCache,
|
||||
ffmpegPngService,
|
||||
tempFilePool,
|
||||
client,
|
||||
logger)
|
||||
{
|
||||
_scannerProxy = scannerProxy;
|
||||
@@ -72,7 +68,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
_libraryRepository = libraryRepository;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -267,7 +262,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -299,7 +293,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -467,7 +460,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -525,7 +517,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -541,7 +532,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -557,7 +547,6 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Xml;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
|
||||
@@ -10,16 +9,13 @@ namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
|
||||
public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILogger<ArtistNfoReader> _logger;
|
||||
|
||||
public ArtistNfoReader(
|
||||
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
|
||||
IClient client,
|
||||
ILogger<ArtistNfoReader> logger)
|
||||
: base(recyclableMemoryStreamManager, logger)
|
||||
{
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -112,7 +108,6 @@ public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return new FailedToReadNfo(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Xml;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
|
||||
@@ -10,16 +9,13 @@ namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
|
||||
public class EpisodeNfoReader : NfoReader<EpisodeNfo>, IEpisodeNfoReader
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILogger<EpisodeNfoReader> _logger;
|
||||
|
||||
public EpisodeNfoReader(
|
||||
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
|
||||
IClient client,
|
||||
ILogger<EpisodeNfoReader> logger)
|
||||
: base(recyclableMemoryStreamManager, logger)
|
||||
{
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -147,7 +143,6 @@ public class EpisodeNfoReader : NfoReader<EpisodeNfo>, IEpisodeNfoReader
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return new FailedToReadNfo(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Xml;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
|
||||
@@ -10,16 +9,13 @@ namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
|
||||
public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILogger<MovieNfoReader> _logger;
|
||||
|
||||
public MovieNfoReader(
|
||||
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
|
||||
IClient client,
|
||||
ILogger<MovieNfoReader> logger)
|
||||
: base(recyclableMemoryStreamManager, logger)
|
||||
{
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -159,7 +155,6 @@ public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return new FailedToReadNfo(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Xml;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
|
||||
@@ -10,16 +9,13 @@ namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
|
||||
public class MusicVideoNfoReader : NfoReader<MusicVideoNfo>, IMusicVideoNfoReader
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILogger<MusicVideoNfoReader> _logger;
|
||||
|
||||
public MusicVideoNfoReader(
|
||||
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
|
||||
IClient client,
|
||||
ILogger<MusicVideoNfoReader> logger)
|
||||
: base(recyclableMemoryStreamManager, logger)
|
||||
{
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -147,7 +143,6 @@ public class MusicVideoNfoReader : NfoReader<MusicVideoNfo>, IMusicVideoNfoReade
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return new FailedToReadNfo(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Xml;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
|
||||
@@ -10,16 +9,13 @@ namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
|
||||
public class OtherVideoNfoReader : NfoReader<OtherVideoNfo>, IOtherVideoNfoReader
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILogger<OtherVideoNfoReader> _logger;
|
||||
|
||||
public OtherVideoNfoReader(
|
||||
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
|
||||
IClient client,
|
||||
ILogger<OtherVideoNfoReader> logger)
|
||||
: base(recyclableMemoryStreamManager, logger)
|
||||
{
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -159,7 +155,6 @@ public class OtherVideoNfoReader : NfoReader<OtherVideoNfo>, IOtherVideoNfoReade
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return new FailedToReadNfo(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Xml;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
|
||||
@@ -10,16 +9,13 @@ namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
|
||||
public class ShowNfoReader : NfoReader<ShowNfo>, IShowNfoReader
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILogger<ShowNfoReader> _logger;
|
||||
|
||||
public ShowNfoReader(
|
||||
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
|
||||
IClient client,
|
||||
ILogger<ShowNfoReader> logger)
|
||||
: base(recyclableMemoryStreamManager, logger)
|
||||
{
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -140,7 +136,6 @@ public class ShowNfoReader : NfoReader<ShowNfo>, IShowNfoReader
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return new FailedToReadNfo(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
@@ -19,7 +18,6 @@ namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScanner
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILocalChaptersProvider _localChaptersProvider;
|
||||
private readonly IMetadataRepository _metadataRepository;
|
||||
@@ -47,7 +45,6 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IFFmpegPngService ffmpegPngService,
|
||||
ITempFilePool tempFilePool,
|
||||
IClient client,
|
||||
ILogger<OtherVideoFolderScanner> logger) : base(
|
||||
fileSystem,
|
||||
localStatisticsProvider,
|
||||
@@ -56,7 +53,6 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
imageCache,
|
||||
ffmpegPngService,
|
||||
tempFilePool,
|
||||
client,
|
||||
logger)
|
||||
{
|
||||
_scannerProxy = scannerProxy;
|
||||
@@ -69,7 +65,6 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
_otherVideoRepository = otherVideoRepository;
|
||||
_libraryRepository = libraryRepository;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -316,7 +311,6 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -332,7 +326,6 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -348,7 +341,6 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -379,7 +371,6 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
@@ -22,7 +21,6 @@ namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolderScanner
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly IScannerProxy _scannerProxy;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
@@ -46,7 +44,6 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IFFmpegPngService ffmpegPngService,
|
||||
ITempFilePool tempFilePool,
|
||||
IClient client,
|
||||
ILogger<RemoteStreamFolderScanner> logger) : base(
|
||||
fileSystem,
|
||||
localStatisticsProvider,
|
||||
@@ -55,7 +52,6 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
|
||||
imageCache,
|
||||
ffmpegPngService,
|
||||
tempFilePool,
|
||||
client,
|
||||
logger)
|
||||
{
|
||||
_scannerProxy = scannerProxy;
|
||||
@@ -66,7 +62,6 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
|
||||
_remoteStreamRepository = remoteStreamRepository;
|
||||
_libraryRepository = libraryRepository;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -337,7 +332,6 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -375,7 +369,6 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -406,7 +399,6 @@ public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolder
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
@@ -19,7 +18,6 @@ namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly IScannerProxy _scannerProxy;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
@@ -43,7 +41,6 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IFFmpegPngService ffmpegPngService,
|
||||
ITempFilePool tempFilePool,
|
||||
IClient client,
|
||||
ILogger<SongFolderScanner> logger) : base(
|
||||
fileSystem,
|
||||
localStatisticsProvider,
|
||||
@@ -52,7 +49,6 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
|
||||
imageCache,
|
||||
ffmpegPngService,
|
||||
tempFilePool,
|
||||
client,
|
||||
logger)
|
||||
{
|
||||
_scannerProxy = scannerProxy;
|
||||
@@ -63,7 +59,6 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
|
||||
_songRepository = songRepository;
|
||||
_libraryRepository = libraryRepository;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -278,7 +273,6 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -332,7 +326,6 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
@@ -19,7 +18,6 @@ namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScanner
|
||||
{
|
||||
private readonly IClient _client;
|
||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILocalChaptersProvider _localChaptersProvider;
|
||||
@@ -48,7 +46,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IFFmpegPngService ffmpegPngService,
|
||||
ITempFilePool tempFilePool,
|
||||
IClient client,
|
||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||
ILogger<TelevisionFolderScanner> logger) : base(
|
||||
fileSystem,
|
||||
@@ -58,7 +55,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
imageCache,
|
||||
ffmpegPngService,
|
||||
tempFilePool,
|
||||
client,
|
||||
logger)
|
||||
{
|
||||
_scannerProxy = scannerProxy;
|
||||
@@ -71,7 +67,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
_metadataRepository = metadataRepository;
|
||||
_libraryRepository = libraryRepository;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_client = client;
|
||||
_fallbackMetadataProvider = fallbackMetadataProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -407,7 +402,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -483,7 +477,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -515,7 +508,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -545,7 +537,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -578,7 +569,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -592,7 +582,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
@@ -606,7 +595,6 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.IO.Abstractions;
|
||||
using Bugsnag;
|
||||
using Bugsnag.Payload;
|
||||
using Dapper;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Emby;
|
||||
@@ -51,7 +49,6 @@ using Serilog.Events;
|
||||
using Serilog.Formatting.Compact;
|
||||
using Testably.Abstractions;
|
||||
using Exception = System.Exception;
|
||||
using IConfiguration = Bugsnag.IConfiguration;
|
||||
|
||||
namespace ErsatzTV.Scanner;
|
||||
|
||||
@@ -254,8 +251,6 @@ public class Program
|
||||
services.AddSingleton<SearchQueryParser>();
|
||||
services.AddSingleton<ISearchIndex, LuceneSearchIndex>();
|
||||
services.AddSingleton<RecyclableMemoryStreamManager>();
|
||||
// TODO: real bugsnag?
|
||||
services.AddSingleton<IClient>(_ => new BugsnagNoopClient());
|
||||
services.AddSingleton<IScannerProxy, ScannerProxy>();
|
||||
services.AddSingleton<ILanguageCodeCache, LanguageCodeCache>();
|
||||
|
||||
@@ -271,43 +266,4 @@ public class Program
|
||||
})
|
||||
.UseSerilog();
|
||||
}
|
||||
|
||||
private class BugsnagNoopClient : IClient
|
||||
{
|
||||
public void Notify(Exception exception)
|
||||
{
|
||||
}
|
||||
|
||||
public void Notify(Exception exception, Middleware callback)
|
||||
{
|
||||
}
|
||||
|
||||
public void Notify(Exception exception, Severity severity)
|
||||
{
|
||||
}
|
||||
|
||||
public void Notify(Exception exception, Severity severity, Middleware callback)
|
||||
{
|
||||
}
|
||||
|
||||
public void Notify(Exception exception, HandledState handledState)
|
||||
{
|
||||
}
|
||||
|
||||
public void Notify(Exception exception, HandledState handledState, Middleware callback)
|
||||
{
|
||||
}
|
||||
|
||||
public void Notify(Report report, Middleware callback)
|
||||
{
|
||||
}
|
||||
|
||||
public void BeforeNotify(Middleware middleware)
|
||||
{
|
||||
}
|
||||
|
||||
public IBreadcrumbs Breadcrumbs => new Breadcrumbs(Configuration);
|
||||
public ISessionTracker SessionTracking => new SessionTracker(Configuration);
|
||||
public IConfiguration Configuration => new Configuration();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
<!-- <PackageReference Include="EntityFrameworkProfiler.Appender" Version="6.0.6049" /> -->
|
||||
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
|
||||
<PackageReference Include="BlazorSortable" Version="5.2.1" />
|
||||
<PackageReference Include="Bugsnag.AspNet.Core" Version="4.1.0" />
|
||||
<PackageReference Include="Chronic.Core" Version="0.4.0" />
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Emby;
|
||||
using ErsatzTV.Core;
|
||||
@@ -71,19 +70,6 @@ public class EmbyService : BackgroundService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process Emby background service request");
|
||||
|
||||
try
|
||||
{
|
||||
using (IServiceScope scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
||||
client.Notify(ex);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Streaming;
|
||||
using ErsatzTV.Application.Troubleshooting;
|
||||
@@ -61,16 +60,6 @@ public class FFmpegWorkerService : BackgroundService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to handle ffmpeg worker request");
|
||||
|
||||
try
|
||||
{
|
||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
||||
client.Notify(ex);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Jellyfin;
|
||||
using ErsatzTV.Core;
|
||||
@@ -71,19 +70,6 @@ public class JellyfinService : BackgroundService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process Jellyfin background service request");
|
||||
|
||||
try
|
||||
{
|
||||
using (IServiceScope scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
||||
client.Notify(ex);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Plex;
|
||||
using ErsatzTV.Core;
|
||||
@@ -74,19 +73,6 @@ public class PlexService : BackgroundService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process plex background service request");
|
||||
|
||||
try
|
||||
{
|
||||
using (IServiceScope scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
||||
client.Notify(ex);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Emby;
|
||||
using ErsatzTV.Application.Jellyfin;
|
||||
@@ -90,17 +89,6 @@ public class ScannerService : BackgroundService
|
||||
catch (Exception ex) when (ex is not (TaskCanceledException or OperationCanceledException))
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process scanner background service request");
|
||||
|
||||
try
|
||||
{
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
||||
client.Notify(ex);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Channels;
|
||||
using ErsatzTV.Application.Emby;
|
||||
@@ -143,19 +142,6 @@ public class SchedulerService : BackgroundService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during scheduler run");
|
||||
|
||||
try
|
||||
{
|
||||
using (IServiceScope scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
||||
client.Notify(ex);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,19 +176,6 @@ public class SchedulerService : BackgroundService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during scheduler run");
|
||||
|
||||
try
|
||||
{
|
||||
using (IServiceScope scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
||||
client.Notify(ex);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
@@ -157,16 +156,6 @@ public class SearchIndexService : BackgroundService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to handle search index batch worker request");
|
||||
|
||||
try
|
||||
{
|
||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
||||
client.Notify(ex);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Channels;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Channels;
|
||||
using ErsatzTV.Application.FFmpeg;
|
||||
@@ -137,16 +136,6 @@ public class WorkerService : BackgroundService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process background service request");
|
||||
|
||||
try
|
||||
{
|
||||
IClient client = scope.ServiceProvider.GetRequiredService<IClient>();
|
||||
client.Notify(ex);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Channels;
|
||||
using BlazorSortable;
|
||||
using Bugsnag.AspNet.Core;
|
||||
using Dapper;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Channels;
|
||||
@@ -132,25 +131,6 @@ public class Startup
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
services.AddBugsnag(configuration =>
|
||||
{
|
||||
configuration.ApiKey = bugsnagConfig.ApiKey;
|
||||
configuration.ProjectNamespaces = new[] { "ErsatzTV" };
|
||||
configuration.AppVersion = Assembly.GetEntryAssembly()
|
||||
?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion ?? "unknown";
|
||||
configuration.AutoNotify = true;
|
||||
|
||||
configuration.NotifyReleaseStages = new[] { "public", "develop" };
|
||||
|
||||
#if DEBUG || DEBUG_NO_SYNC
|
||||
configuration.ReleaseStage = "develop";
|
||||
#else
|
||||
// effectively "disable" by tweaking app config
|
||||
configuration.ReleaseStage = bugsnagConfig.Enable ? "public" : "private";
|
||||
#endif
|
||||
});
|
||||
|
||||
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(FileSystemLayout.DataProtectionFolder));
|
||||
|
||||
services.AddOpenApi("v1", options => { options.ShouldInclude += a => a.GroupName == "general"; });
|
||||
@@ -774,7 +754,6 @@ public class Startup
|
||||
services.AddScoped<IFileNotFoundHealthCheck, FileNotFoundHealthCheck>();
|
||||
services.AddScoped<IUnavailableHealthCheck, UnavailableHealthCheck>();
|
||||
services.AddScoped<IVaapiDriverHealthCheck, VaapiDriverHealthCheck>();
|
||||
services.AddScoped<IErrorReportsHealthCheck, ErrorReportsHealthCheck>();
|
||||
services.AddScoped<IUnifiedDockerHealthCheck, UnifiedDockerHealthCheck>();
|
||||
services.AddScoped<IDowngradeHealthCheck, DowngradeHealthCheck>();
|
||||
services.AddScoped<IEmptyScheduleHealthCheck, EmptyScheduleHealthCheck>();
|
||||
|
||||
@@ -15,12 +15,5 @@
|
||||
"WithThreadId"
|
||||
]
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Trakt": {
|
||||
"ClientId": "e30585c7db49eaf1bd80d7ce5296d5de0bb33e1166f323cd7202412a605c609a"
|
||||
},
|
||||
"Bugsnag": {
|
||||
"ApiKey": "f59f3cc93cce91210a5c0f047eb2047c",
|
||||
"Enable": true
|
||||
}
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-amd64 AS dotnet-runtime
|
||||
|
||||
FROM --platform=linux/amd64 ghcr.io/ersatztv/ersatztv-ffmpeg:7.1.1 AS runtime-base
|
||||
FROM --platform=linux/amd64 192.168.1.95:3000/timothy/ersatztv-ffmpeg:7.1.1 AS runtime-base
|
||||
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends python3 python3-pip && \
|
||||
@@ -24,6 +24,10 @@ COPY scripts/scripted-schedules/. /app/scripted-schedules/
|
||||
|
||||
# copy csproj and restore as distinct layers
|
||||
COPY *.sln .
|
||||
# repo-wide build config (MSBuild props/targets incl. NuGet-audit warning
|
||||
# exemptions, SDK pin, analyzer severities) must be present before restore so the
|
||||
# image build matches local/CI builds
|
||||
COPY Directory.Build.props Directory.Build.targets global.json .editorconfig ./
|
||||
COPY artwork/* ./artwork/
|
||||
COPY ErsatzTV/*.csproj ./ErsatzTV/
|
||||
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Channel Architecture
|
||||
|
||||
## Channel Entity
|
||||
|
||||
Defined in `ErsatzTV.Core/Domain/Channel.cs`. Key fields:
|
||||
|
||||
- **Identity**: `Number` (e.g., "1", "2.1"), `Name`, `UniqueId` (GUID for M3U/XMLTV)
|
||||
- **Encoding**: `FFmpegProfileId` — video/audio codec, bitrate, resolution, hardware acceleration
|
||||
- **Streaming**: `StreamingMode` (TransportStream, HLS Direct, HLS Segmenter, TS Hybrid)
|
||||
- **Behavior**: `PlayoutMode` (Continuous vs OnDemand), `IdleBehavior` (StopOnDisconnect vs KeepRunning)
|
||||
- **Visual**: `WatermarkId`, `FallbackFillerId`, artwork (logos)
|
||||
- **Mirroring**: `PlayoutSource` (Generated vs Mirror) — a mirror channel copies another with optional time offset
|
||||
- **Display**: `Group`, `Categories`, `ShowInEpg`, `IsEnabled`
|
||||
- **Audio/Subtitle defaults**: preferred language codes, subtitle mode
|
||||
|
||||
## Content Sources
|
||||
|
||||
Channels get content through a **Playout** → **ProgramSchedule** → **ProgramScheduleItem** chain.
|
||||
|
||||
### Collection Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Collection` | Manual grouping of media items with custom playback order |
|
||||
| `MultiCollection` | Aggregate of collections + smart collections |
|
||||
| `SmartCollection` | Query-based (Lucene.Net) dynamic filtering |
|
||||
| `Playlist` | Ordered items with per-item config (count, fillers, playback order) |
|
||||
| `TelevisionShow` / `TelevisionSeason` | Structured TV hierarchy |
|
||||
| `Movie`, `Episode`, `MusicVideo`, `OtherVideo`, `Song`, `Image` | Individual media items |
|
||||
| `RerunCollection` | Wraps any collection with separate first-run/rerun playback orders |
|
||||
| `SearchQuery` | Dynamic results from a search |
|
||||
| `RemoteStream` | External stream URLs |
|
||||
|
||||
### Media Sources
|
||||
|
||||
Media items are imported from configured libraries (Jellyfin, Plex, Emby, or local filesystem). Each source type has its own entity variants (e.g., `JellyfinMovie`, `PlexEpisode`).
|
||||
|
||||
## Scheduling
|
||||
|
||||
### Schedule Kinds
|
||||
|
||||
- **Classic**: Traditional ProgramSchedule with items — the most common
|
||||
- **Block**: Template-based block scheduling
|
||||
- **Sequential**: Strict sequential ordering
|
||||
- **Scripted**: External script-driven playout
|
||||
- **ExternalJson**: Playout defined by external JSON file
|
||||
|
||||
### Schedule Item Types
|
||||
|
||||
Each `ProgramScheduleItem` is one of four concrete types:
|
||||
|
||||
1. **One** — Play exactly 1 item per cycle
|
||||
2. **Multiple** — Play N items (fixed count, collection size, or playlist item size)
|
||||
3. **Duration** — Fill a time window (with tail mode: none, offline, slate, or filler)
|
||||
4. **Flood** — Play items continuously until the next fixed-start item
|
||||
|
||||
Items can have `StartType` of Fixed (anchored to clock time) or Dynamic (follows previous item).
|
||||
|
||||
### Playback Orders
|
||||
|
||||
`Chronological`, `Random`, `Shuffle`, `ShuffleInOrder`, `MultiEpisodeShuffle`, `SeasonEpisode`, `RandomRotation`, `Marathon` (group by show/season/artist/album/director).
|
||||
|
||||
### Filler System
|
||||
|
||||
`FillerPreset` defines content to fill gaps. Each schedule item can have:
|
||||
- **PreRoll** — before main content
|
||||
- **MidRoll** — during (chapter breaks)
|
||||
- **PostRoll** — after main content
|
||||
- **Tail** — pad remaining time in a duration block
|
||||
- **Fallback** — channel-level default when nothing else available
|
||||
|
||||
Filler modes: Duration, Count, Pad (to nearest minute), RandomCount.
|
||||
|
||||
### Alternate Schedules
|
||||
|
||||
`ProgramScheduleAlternate` overrides the main schedule for specific days of week, days of month, months of year, or date ranges. Useful for seasonal programming or weekend variations.
|
||||
|
||||
## Playout Pipeline
|
||||
|
||||
```
|
||||
Channel
|
||||
└── Playout
|
||||
├── ProgramSchedule
|
||||
│ └── ProgramScheduleItems (One|Multiple|Duration|Flood)
|
||||
│ └── Content source (Collection, Playlist, SmartCollection, etc.)
|
||||
├── PlayoutItems (generated — the actual timeline)
|
||||
│ └── MediaItem + start/finish times + filler kind + watermarks
|
||||
├── PlayoutGaps (time periods with no content)
|
||||
└── ProgramScheduleAlternates (day/date overrides)
|
||||
```
|
||||
|
||||
The scheduling engine (`ErsatzTV.Core/Scheduling/`) resolves schedule items into concrete `PlayoutItem` entries with precise start/finish times. Each `PlayoutItem` references a specific `MediaItem` and includes trim points (`InPoint`/`OutPoint`), filler classification, and per-item audio/subtitle overrides.
|
||||
|
||||
## Watermarks
|
||||
|
||||
`ChannelWatermark` supports modes: Permanent, Intermittent, OpacityExpression. Image sources: custom upload, channel logo, or built-in resource. Positioned with percentage-based margins and z-index.
|
||||
|
||||
Note: `ChannelLogoGenerator.GenerateChannelLogoUrl()` hardcodes `localhost` for watermark logo fetching — see issue #1 for details.
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# CI/CD for the ErsatzTV Fork
|
||||
|
||||
The fork builds its own Docker image via **Gitea Actions** on the homelab and pushes
|
||||
to the **Gitea container registry**. Runner + registry were provisioned in
|
||||
server-management#172; the build pipeline is ersatztv#4; test/prod containers are
|
||||
server-management#481.
|
||||
|
||||
## Versioning & releases
|
||||
|
||||
The fork inherits upstream ErsatzTV's scheme: **`vYY.<release-seq>.<patch>`** (lightweight, `v`-prefixed git tags).
|
||||
|
||||
- **`YY`** — two-digit year.
|
||||
- **`<release-seq>`** — a sequential release counter **within the year**, reset at each year boundary. It is **not** the calendar month. (Evidence: `v25.2.0` shipped in June 2025, `v25.5.0` in Sep, `v26.3.0` in Feb 2026 — minors don't track months; and `v25.9.0` → `v26.1.0` shows the year-reset.)
|
||||
- **`<patch>`** — a small follow-up/hotfix on the *same* release line (e.g. `v26.1.0` → `v26.1.1`, days later).
|
||||
|
||||
Upstream's final release was **`v26.3.0`** (archived). Our line continues from there:
|
||||
|
||||
| Tag | Meaning |
|
||||
|-----|---------|
|
||||
| `v26.3.1` | Upstream 26.3.0 **rebuilt on our infra** (Gitea CI/registry, fork ffmpeg base) — **no application changes**. A patch bump, because nothing functional changed. |
|
||||
| `v26.4.0` | Reserved for our **first release that carries actual app changes** (e.g. the #1 M3U fix). Later 2026 releases: `26.5.0`, `26.6.0`, …; a new year resets to `27.1.0`. |
|
||||
|
||||
**Cutting a release:** push a `vYY.N.P` tag on `main` → CI builds `:prod` + `:<version>` + `:<sha>`; server-management does the prod switch (server-management#481).
|
||||
|
||||
**Gotcha:** never put a `[skip ci]` token in a commit you intend to tag — Gitea reads skip-ci from the *tagged* commit and will **suppress the release build**. (Also, `workflow_dispatch` on a tag ref isn't supported on this Gitea version, so the tag *push* must do the triggering.) Release commits, and anything you'll tag, must not contain skip-ci.
|
||||
|
||||
## The workflow: `.gitea/workflows/docker-build.yml`
|
||||
|
||||
Single workflow, two jobs (`test` → `build`).
|
||||
|
||||
### Triggers & tags
|
||||
|
||||
| Trigger | `test` job | `build` job | Image tags pushed |
|
||||
|---------|:----------:|:-----------:|-------------------|
|
||||
| `pull_request` | ✅ | — (skipped) | none |
|
||||
| push to `main` | ✅ | ✅ | `:latest` + `:<short-sha>` |
|
||||
| push tag `v*` | ✅ | ✅ | `:prod` + `:<version>` + `:<short-sha>` |
|
||||
| `workflow_dispatch` | ✅ | ✅ | only if ref is `main`/`v*`, else build-only (no push) |
|
||||
|
||||
`:latest` is the **test/dev** channel (every `main` commit). Prod pins **`:prod`**,
|
||||
never `:latest` — enforced in the prod compose (server-management#481). `:prod` is
|
||||
only produced by pushing a `v*` tag.
|
||||
|
||||
`concurrency: { group: ersatztv-build, cancel-in-progress: false }` serializes all
|
||||
runs — the single runner on jazz can't safely run the push-`main`-then-push-`v*`
|
||||
release flow in parallel (shared `:buildcache` tag, shared smoke container).
|
||||
|
||||
### `test` job
|
||||
|
||||
`dotnet restore` → strip the Scanner project ref (`sed -i '/Scanner/d'`, matching the
|
||||
Docker build) → `dotnet build -c Release` → `dotnet test -c Release --no-build`. Gates
|
||||
the image build.
|
||||
|
||||
### `build` job
|
||||
|
||||
1. Compute `INFO_VERSION` (`git describe` + short sha on `main`; tag version on `v*`).
|
||||
2. `docker/setup-buildx-action` with `buildkitd-config-inline` setting `http = true`
|
||||
for `192.168.1.95:3000` — **BuildKit does not inherit the host daemon's
|
||||
`insecure-registries`**, so without this, cache/base-image/push over the HTTP
|
||||
registry fails (`http: server gave HTTP response to HTTPS client`).
|
||||
3. `docker/login-action` with repo secrets `REGISTRY_USER` / `REGISTRY_PASSWORD`.
|
||||
4. `docker/build-push-action@v6`: amd64-only, `docker/Dockerfile`, `INFO_VERSION`
|
||||
build-arg, registry layer cache (`type=registry,ref=…:buildcache`,
|
||||
`cache-to … ignore-error=true`).
|
||||
5. **Smoke test**: pull the just-pushed `:<sha>`, run it, and poll ErsatzTV's web
|
||||
server from inside the container (`docker exec … python3` → `http://localhost:8409/`).
|
||||
Unique container name + `trap … EXIT` cleanup. Fails if it never serves HTTP.
|
||||
|
||||
## Dockerfile notes (`docker/Dockerfile`)
|
||||
|
||||
- Base image: **`192.168.1.95:3000/timothy/ersatztv-ffmpeg:7.1.1`** (our Gitea fork of
|
||||
the archived `ghcr.io/ersatztv/ersatztv-ffmpeg`). FFmpeg 8 upgrade is backlogged:
|
||||
base image → ersatztv-ffmpeg#4, app-side compat → ersatztv#9.
|
||||
- Copies `Directory.Build.props`, `Directory.Build.targets`, `global.json`,
|
||||
`.editorconfig` before `dotnet restore` so the image build uses the same MSBuild
|
||||
config, SDK pin, and analyzer severities as local/CI builds (it previously copied
|
||||
only `*.sln`).
|
||||
- amd64-only (jazz is x86_64). No arm32/arm64, no DMG/exe artifacts, no GHCR/DockerHub.
|
||||
|
||||
## NuGet audit
|
||||
|
||||
.NET 10 runs NuGet audit on restore. Several projects set `TreatWarningsAsErrors=true`,
|
||||
so vulnerable transitive packages failed the build. `Directory.Build.props` demotes
|
||||
low/moderate/high advisories (NU1901-1903) to warnings and promotes NU1904 (critical)
|
||||
to an error in **every** project via `WarningsAsErrors`. Underlying vulnerable deps are
|
||||
tracked in ersatztv#8.
|
||||
|
||||
## Registry
|
||||
|
||||
Gitea Packages, HTTP-only at `192.168.1.95:3000`. jazz's Docker daemon has it as an
|
||||
insecure-registry (server-management#172). Images: `192.168.1.95:3000/timothy/ersatztv:<tag>`.
|
||||
|
||||
## Test / prod environments
|
||||
|
||||
Container/compose wiring lives in **server-management** (project boundary): test
|
||||
`ersatztv-test` on 8410 (`:latest`), prod `ersatztv` on 8409 (`:prod`). See
|
||||
server-management#481 for the full spec (registry pull on jazz, volumes, Jellyfin
|
||||
isolation for test, Watchtower/manual promotion).
|
||||
|
||||
## Retired upstream workflows
|
||||
|
||||
The upstream `.github/workflows/` (`ci.yml`, `docker.yml`, `artifacts.yml`,
|
||||
`release.yml`, `pr.yml`, `issue-stale.yml`) were removed — they targeted
|
||||
GHCR/DockerHub + Azure/Apple signing and called reusable workflows at dead
|
||||
`ersatztv/ersatztv@main` paths, and ran as noise (incl. a daily stale-issue cron) on
|
||||
the Gitea runner. Upstream is archived, so there are no future merges to preserve them
|
||||
for. The dead `.github/dependabot.yml` and `FUNDING.yml` (upstream-pointed) were also
|
||||
removed.
|
||||
|
||||
## Known follow-ups
|
||||
|
||||
- Pin third-party actions to commit SHAs (currently floating major tags cloned from
|
||||
github.com at runtime) — low priority for a homelab; tracked informally.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Fork Maintenance Strategy
|
||||
|
||||
Upstream ErsatzTV was archived February 2026 at v26.3.0. This fork is maintained independently on [Gitea](http://192.168.1.95:3000/timothy/ersatztv).
|
||||
|
||||
## Divergence Policy
|
||||
|
||||
We diverge freely from upstream's final state. There is no upstream to merge from, so maintaining merge compatibility serves no purpose. All changes are our own.
|
||||
|
||||
## Security & Dependency Updates
|
||||
|
||||
### .NET Runtime
|
||||
|
||||
- **Current**: .NET 10.0 (LTS candidate, supported through Nov 2028)
|
||||
- **Upgrade path**: When .NET 11 ships (Nov 2026), upgrade by updating `TargetFramework` across all projects and `global.json`. The `rollForward: latestMinor` setting in `global.json` handles patch versions automatically.
|
||||
- **Key constraint**: EF Core is pinned to `[9.0.12,10)` — a .NET 11 upgrade will likely require bumping to EF Core 10.x simultaneously.
|
||||
|
||||
### NuGet Packages
|
||||
|
||||
- No central package management (`Directory.Packages.props`) — versions are declared per-project. This means bulk updates require editing multiple .csproj files.
|
||||
- Upstream had a GitHub Dependabot config (`.github/dependabot.yml`) that is not active on Gitea.
|
||||
- **Current approach**: Manual periodic audits. Run `dotnet list package --outdated` to check for updates.
|
||||
- **Future consideration**: Add a Gitea Actions workflow for dependency scanning, or adopt `Directory.Packages.props` to centralize version management.
|
||||
|
||||
### Docker Base Images
|
||||
|
||||
- .NET SDK/runtime images (`mcr.microsoft.com/dotnet/sdk:10.0-noble-amd64`) — update when .NET patches ship.
|
||||
- FFmpeg image: forked separately at [timothy/ersatztv-ffmpeg](http://192.168.1.95:3000/timothy/ersatztv-ffmpeg). Currently `192.168.1.95:3000/timothy/ersatztv-ffmpeg:7.1.1`. The main Dockerfile still references the upstream `ghcr.io` image and needs updating.
|
||||
|
||||
### CVE Response
|
||||
|
||||
1. Check if the CVE affects a dependency we use (most NuGet advisories are noise)
|
||||
2. Update the package version in the relevant .csproj file(s)
|
||||
3. Build, run tests, deploy to test environment (port 8410)
|
||||
4. Promote to prod after verification
|
||||
|
||||
## EF Core Migrations
|
||||
|
||||
- **SQLite**: 196 migrations (primary, Feb 2021 – Feb 2026)
|
||||
- **MySQL**: 153 migrations (secondary, Aug 2023 – Feb 2026, parity maintained)
|
||||
|
||||
### Adding New Migrations
|
||||
|
||||
```bash
|
||||
# SQLite (primary)
|
||||
dotnet ef migrations add MigrationName \
|
||||
--project ErsatzTV.Infrastructure.Sqlite \
|
||||
--startup-project ErsatzTV
|
||||
|
||||
# MySQL (if maintaining parity)
|
||||
dotnet ef migrations add MigrationName \
|
||||
--project ErsatzTV.Infrastructure.MySql \
|
||||
--startup-project ErsatzTV
|
||||
```
|
||||
|
||||
We only use SQLite in the homelab. MySQL migrations can be maintained for completeness but are not tested in deployment.
|
||||
|
||||
## Feature Development
|
||||
|
||||
New features follow the existing CQRS/MediatR pattern. No compatibility constraints — we own the entire codebase now. Track work via [Gitea Issues](http://192.168.1.95:3000/timothy/ersatztv/issues).
|
||||
|
||||
## Key Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| EF Core major version gap | Pin to `[9.x,10)` range; bump when .NET upgrade forces it |
|
||||
| Lucene.Net stuck on beta (`4.8.0-beta00017`) | Monitor for stable release; functional as-is |
|
||||
| SkiaSharp native deps | Pinned with `NativeAssets.Linux.NoDependencies`; test on Linux after updates |
|
||||
| OpenAPI generator JAR (`7.15.0`) | Hardcoded in Dockerfile; update manually when needed |
|
||||
@@ -0,0 +1,124 @@
|
||||
# M3U/XMLTV Integration
|
||||
|
||||
## Overview
|
||||
|
||||
ErsatzTV serves M3U playlists and XMLTV guide data via HTTP endpoints. Jellyfin consumes these as an IPTV tuner source.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Route | Purpose |
|
||||
|-------|---------|
|
||||
| `GET /iptv/channels.m3u` | M3U playlist (channel list + stream URLs) |
|
||||
| `GET /iptv/xmltv.xml` | XMLTV guide (EPG data) |
|
||||
| `GET /iptv/logos/{fileName}.jpg` | Uploaded channel logos |
|
||||
| `GET /iptv/logos/gen?text={name}` | Generated text logos (SkiaSharp, 200x100 PNG) |
|
||||
| `GET /iptv/channel/{number}.ts` | Transport stream |
|
||||
| `GET /iptv/channel/{number}.m3u8` | HLS stream |
|
||||
|
||||
All defined in `ErsatzTV/Controllers/IptvController.cs` (streams) and `ErsatzTV/Controllers/ArtworkController.cs` (logo generation).
|
||||
|
||||
## M3U Generation
|
||||
|
||||
**Code**: `ErsatzTV.Core/Iptv/ChannelPlaylist.cs` → `ToM3U()`
|
||||
|
||||
The controller captures `Request.Scheme`, `Request.Host`, and `Request.PathBase` from the incoming HTTP request and passes them through MediatR to the playlist builder. All URLs in the M3U output use the request-derived host — they are **not** hardcoded.
|
||||
|
||||
Each channel entry includes:
|
||||
- `tvg-id` — channel number as identifier
|
||||
- `tvg-chno` — channel number for ordering
|
||||
- `tvg-name` — channel display name
|
||||
- `tvg-logo` — logo URL (uploaded artwork or generated text logo)
|
||||
- `tvc-stream-vcodec` / `tvc-stream-acodec` — codec hints from FFmpeg profile
|
||||
- `CUID` — base64-encoded UniqueId
|
||||
- `group-title` — channel group for categorization
|
||||
- Stream URL (format depends on channel's streaming mode)
|
||||
|
||||
The XMLTV guide URL is included as an `#EXTM3U` header attribute.
|
||||
|
||||
## XMLTV Generation
|
||||
|
||||
**Code**: `ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs`
|
||||
|
||||
XMLTV data is pre-generated from Scriban templates (`ErsatzTV/Resources/Templates/_channel.sbntxt`) and cached as XML fragments. At request time, the handler:
|
||||
|
||||
1. Reads cached fragments from disk
|
||||
2. Substitutes `{RequestBase}` with the actual `scheme://host/base` from the request
|
||||
3. Substitutes `{AccessTokenUri}` with version + optional auth token
|
||||
4. Filters channels by `ShowInEpg`
|
||||
5. Combines channel list + program entries into the final XML
|
||||
|
||||
## Jellyfin Integration
|
||||
|
||||
### Tuner Setup
|
||||
|
||||
In Jellyfin, add an IPTV tuner pointing to:
|
||||
```
|
||||
http://ersatztv:8409/iptv/channels.m3u
|
||||
```
|
||||
|
||||
Jellyfin reads the M3U to discover channels and their stream URLs. The `Host` header in Jellyfin's request determines what host appears in all embedded URLs — so the hostname used in the tuner URL matters.
|
||||
|
||||
### Guide Data
|
||||
|
||||
Jellyfin fetches the XMLTV URL embedded in the M3U header for EPG data. This provides program titles, descriptions, and timing for the TV guide.
|
||||
|
||||
### Guide Refresh
|
||||
|
||||
Jellyfin periodically refreshes the guide data. Channel logos are fetched from the `tvg-logo` URLs in the M3U. If those URLs are unreachable from Jellyfin's network context, logos break.
|
||||
|
||||
## Logo URL Issue (Gitea #1)
|
||||
|
||||
### The Problem
|
||||
|
||||
Channel logos break after Jellyfin guide refreshes. The issue was originally reported as hardcoded `localhost` in `tvg-logo` URLs.
|
||||
|
||||
### Investigation Findings
|
||||
|
||||
The M3U and XMLTV generation paths **correctly** use request-derived host:
|
||||
- `ChannelPlaylist.ToM3U()` builds logo URLs from `_scheme`, `_host`, `_baseUrl` (all from the request)
|
||||
- `GetChannelGuideHandler` substitutes `{RequestBase}` at request time
|
||||
|
||||
The **actual** hardcoded `localhost` is in `ChannelLogoGenerator.GenerateChannelLogoUrl()` (`ErsatzTV.Core/Images/ChannelLogoGenerator.cs`, line 82):
|
||||
```csharp
|
||||
$"http://localhost:{Settings.StreamingPort}{GetRoute}?{GetRouteQueryParamName}={channel.WebEncodedName}"
|
||||
```
|
||||
|
||||
This method is called only by `WatermarkSelector.cs` (lines 219, 268, 317, 385) — for watermark overlays during transcoding, **not** for M3U/XMLTV output. Since FFmpeg runs on the same host as ErsatzTV, `localhost` is correct for watermarks.
|
||||
|
||||
### Confirmed Root Cause (2026-06-26)
|
||||
|
||||
Live experiment against the running `ersatztv` container on jazz (192.168.1.99:8409) proves the host in `tvg-logo` is **entirely a function of the incoming `Host` header** — there is no hardcoded `localhost` in the M3U/XMLTV path:
|
||||
|
||||
```
|
||||
curl -H "Host: localhost:8409" .../iptv/channels.m3u -> tvg-logo="http://localhost:8409/iptv/logos/...jpg"
|
||||
curl -H "Host: ersatztv:8409" .../iptv/channels.m3u -> tvg-logo="http://ersatztv:8409/iptv/logos/...jpg"
|
||||
```
|
||||
|
||||
So the `http://localhost:8409` URLs that broke in Jellyfin were baked in because a client fetched ErsatzTV using `Host: localhost:8409` (a misconfigured tuner/XMLTV URL in Jellyfin's Live TV settings, or a proxy passing that Host). The original issue framing ("hardcoded localhost in code") is a red herring — see Investigation Findings above.
|
||||
|
||||
This is therefore **not a code bug in the M3U/XMLTV generators**. There are two complementary fixes:
|
||||
|
||||
1. **Operational (server-management):** Jellyfin's M3U tuner URL and XMLTV listing URL must use a host that is resolvable *and* correct from Jellyfin's network context (e.g. `http://ersatztv:8409`), never `localhost`. Owned by `server-management` (related: its issue #171).
|
||||
2. **Durable hardening (ersatztv, deferred — Gitea #1):** add an optional advertised **Base URL** override so generated URLs stop depending on the fragile `Host` header. See below.
|
||||
|
||||
### Architecture: absolute-URL generation (relevant for future work)
|
||||
|
||||
**ErsatzTV has no "advertised/public/base URL" setting.** Every absolute URL it emits is derived from the *incoming request's* `Scheme` + `Host` + `PathBase`:
|
||||
|
||||
| Output | Source of host |
|
||||
|--------|----------------|
|
||||
| M3U stream URLs & `tvg-logo` | `ChannelPlaylist` ctor args `_scheme`/`_host`/`_baseUrl`, captured in `IptvController.GetChannelPlaylist` from `Request.Scheme`/`Request.Host`/`Request.PathBase` |
|
||||
| XMLTV channel `<icon>` and programme artwork | `{RequestBase}` placeholder in the cached `.xml` fragments, substituted in `GetChannelGuideHandler` with `request.Scheme://request.Host{request.BaseUrl}` |
|
||||
| FFmpeg watermark overlay logo | `ChannelLogoGenerator.GenerateChannelLogoUrl()` — hardcoded `http://localhost:{StreamingPort}` (correct: FFmpeg runs in-container). **Not** part of M3U/XMLTV. |
|
||||
|
||||
Implications for future work:
|
||||
- Any client that connects via a different host (localhost, a reverse-proxy alias, split-DNS name) gets that host echoed back into stream + logo + artwork URLs. This affects the planned REST API (#2) and any reverse-proxy fronting (server-management) too.
|
||||
- **Settings are stored in the `ConfigElement` table as simple key/value** (`ConfigElementKey` in `ErsatzTV.Core/Domain/ConfigElementKey.cs`; read/write via `IConfigElementRepository.GetValue<T>` / upsert). Adding a new setting needs **no EF migration**.
|
||||
|
||||
### Planned fix for #1 (deferred until CI/CD #3/#4 land)
|
||||
|
||||
Add an optional `iptv.base_url` ConfigElement + a "Base URL" field on the settings page. When set, `GetChannelPlaylistHandler` and `GetChannelGuideHandler` parse it into scheme/host/base and use it instead of the request values; when blank, behaviour is unchanged (request-derived). Cover with unit tests on `ChannelPlaylist.ToM3U()` (pure class — easy to assert logo/stream hosts) plus the override-resolution logic. Run the existing suite before and after to guard against regressions.
|
||||
|
||||
### Current Workaround
|
||||
|
||||
A script downloads logos from ErsatzTV and base64-uploads them directly to Jellyfin's `/Items/{id}/Images/Primary` API, bypassing the M3U logo URLs entirely. Documented in `server-management` repo (issue #171).
|
||||
Reference in New Issue
Block a user