Compare commits

..
Author SHA1 Message Date
Jason Dove d91e945124 update changelog for release v0.8.3-beta [no ci] 2023-11-22 11:36:31 -06:00
Jason DoveandGitHub 9dabffbac1 support more formats for show fallback metadata (#1514) 2023-11-21 15:52:25 -06:00
Jason DoveandGitHub d310b5c09d fix nvidia hardware decoding on windows (#1513) 2023-11-21 06:36:05 -06:00
Jason DoveandGitHub ba48b3a676 update dependencies (#1512) 2023-11-20 21:57:43 -06:00
Jason DoveandGitHub d8a51b5d6d fix season display bug (#1511) 2023-11-20 21:17:11 -06:00
Jason DoveandGitHub 97674cff89 fix bug scheduling duration filler (#1510) 2023-11-20 21:02:26 -06:00
Jason DoveandGitHub 4820615308 proper fix to the sdk mismatch (#1509) 2023-11-16 13:37:20 -06:00
Jason DoveandGitHub 1ddf27ce88 pin dotnet sdk version used in github actions (#1508) 2023-11-16 13:21:51 -06:00
Jason DoveandGitHub cd98a89acd enable docker arm builds again (#1507) 2023-11-16 13:07:49 -06:00
Jason DoveandGitHub a2a6afc3e3 temp disable arm docker builds (#1506) 2023-11-16 09:58:46 -06:00
Jason DoveandGitHub dfaba8c7b0 use release version of ffmpeg 6.1 (#1505) 2023-11-16 09:57:13 -06:00
Jason DoveandGitHub 5d11a6b46f use separate model for plex collection scanning since the api types are inconsistent (#1504) 2023-11-16 06:43:48 -06:00
Jason DoveandGitHub b95a89b11f plex collection rework (#1503)
* start to rework plex collection scanning

* sync plex collections to db

* sync plex collection items

* update changelog
2023-11-14 10:41:21 -06:00
Jason DoveandGitHub 948b3735bd fix file not found music videos (#1502)
* fix indexing music videos in file not found state

* update dependencies
2023-11-14 05:50:51 -06:00
Jason DoveandGitHub 5ecf271773 fix jellyfin library scan (#1501)
* update dependencies

* fix jellyfin library scan
2023-11-10 06:26:23 -06:00
Jason DoveandGitHub b287c0d6ec add jellyfin season number fallback (#1497) 2023-11-06 09:37:12 -06:00
Jason DoveandGitHub b667659c05 use notarytool directly instead of gon (#1493) 2023-11-05 07:46:15 -06:00
Jason DoveandGitHub 22d3025e8e include noto cjk fonts in docker (#1492) 2023-11-05 06:15:57 -06:00
Jason DoveandGitHub 8f5b181372 mysql media server library scan fixes (#1491)
* fix some mysql movie library updates

* fix some mysql show library updates

* update dependencies
2023-10-30 06:45:00 -05:00
Jason DoveandGitHub f5060522aa windows nvidia h264 workaround (#1487)
* work around bad h264_cuvid behavior on windows with ffmpeg snapshot

* use latest ffmpeg build on windows

* nvdec => cuda
2023-10-16 11:40:12 -05:00
Jason DoveandGitHub 14a88bd225 optimize ffmpeg capability cache (#1486)
* minimize cached ffmpeg capabilities

* use set intersect

* try disabling work ahead on nvidia/windows
2023-10-16 08:42:26 -05:00
Jason DoveandGitHub 0550c60a78 allow older ffmpeg for testing (#1485)
* allow older ffmpeg for testing

* use proper option name
2023-10-14 21:13:18 -05:00
Jason DoveandGitHub d3bdcf9bc4 allow plex personal media show libraries (#1483) 2023-10-13 13:33:10 -05:00
Jason DoveandGitHub 714f68a887 add language_tag and seconds fields to search index (#1479)
* add `language_tag` and `seconds` fields to search index

* simplify
2023-10-10 20:36:50 -05:00
Jason DoveandGitHub 17bed524f2 fix ui display of multiple languages (#1474) 2023-10-08 18:21:48 -05:00
Jason DoveandGitHub c3fe263978 validate hardware accel, use hw accel for error messages (#1471)
* only display supported hw accels in ffmpeg profile editor

* qsv capability improvements

* qsv fixes

* update changelog
2023-10-08 11:21:04 -05:00
Jason DoveandGitHub 5291832e6c fix clipboard and logs (#1466)
* fix copy to clipboard in some cases

* improve subtitle language selection logging

* log playout item details
2023-10-06 19:36:42 -05:00
Jason DoveandGitHub b39dd693f0 update dependencies and windows ffmpeg (#1462)
* update dependencies

* update windows ffmpeg version
2023-10-05 19:14:06 -05:00
Jason DoveandGitHub 46bf9ef990 fix intel vaapi pgs subtitle pixel format (#1455) 2023-09-30 13:10:23 -05:00
Jason DoveandGitHub bc845b1327 schedule filler using ticks instead of milliseconds (#1454)
* add script to set db provider

* don't extract embedded subtitles with DEBUG_NO_SYNC

* fix playout filler precision bug
2023-09-30 06:41:15 -05:00
Jason DoveandGitHub 3ab8e5bc3a optimize jellyfin collection scanning (#1453) 2023-09-29 09:47:57 -05:00
Jason DoveandGitHub e8bc051f73 transcoding improvements (#1452)
* use noautoscale with vaapi encoder

* only use one input file for vaapi with radeonsi driver

* fix vaapi 8-bit to 10-bit

* fix nvidia subtitle scaling

* optimize nvidia subtitle scaling

* fix test pgs subtitle
2023-09-29 06:29:59 -05:00
Jason DoveandGitHub b008fcfd85 fix scheduling precision error (#1451)
* fix scheduling precision error

* update dependencies
2023-09-27 06:07:48 -05:00
Jason DoveandGitHub 547db5fb51 add kodiprop to channels.m3u (#1448) 2023-09-26 15:47:55 -05:00
Jason DoveandGitHub 58fae1b0cc add crop scaling behavior (#1443)
* add scaling behavior - crop

* fix ffmpeg version check on windows (snapshot)

* update dependencies
2023-09-22 08:23:49 -05:00
Jason DoveandGitHub 694b6bbd91 scaling behavior and normalize loudness (#1439)
* update changelog [no ci]

* add ffmpeg profile scaling behavior

* update dependencies

* add normalize loudness mode

* update changelog
2023-09-21 02:46:43 -05:00
Jason DoveandGitHub e0f8b7d7ae use ffmpeg 6.1 snapshot for windows (#1435) 2023-09-14 19:33:40 -05:00
Jason DoveandGitHub b16215fcd6 improve hls throttle (#1434)
* throttle using ffmpeg option

* update ffmpeg version
2023-09-14 19:28:15 -05:00
Jason Dove 85f2c658aa update changelog for release v0.8.2-beta [no ci] 2023-09-14 09:19:21 -05:00
Jason DoveandGitHub 78356314e6 update dependencies (#1433) 2023-09-14 08:46:59 -05:00
Jason DoveandGitHub b00a25bbee fix parsing show title from nfo (#1426) 2023-09-10 19:46:51 -05:00
4d77576be2 update dependencies (#1425)
Co-authored-by: Jason <jason@mbp-touch.local>
2023-09-10 09:22:35 -05:00
Jason DoveandGitHub a90348740d fix subsequent hls session work ahead (#1419) 2023-09-06 20:01:58 -05:00
Jason DoveandGitHub 8081845ef1 fix adding alternate schedule (#1418) 2023-09-04 14:06:39 -05:00
Jason DoveandGitHub d014eb4274 mysql ui fixes (#1417) 2023-09-04 13:12:00 -05:00
Jason DoveandGitHub 8c9cf7b6f2 fix mid roll pad; fix mysql queries (#1416)
* fix mysql queries

* fix mid roll pad with hls segmenter
2023-09-04 10:01:39 -05:00
Jason DoveandGitHub 5d9c8d4f7d update checkout actions (#1415) 2023-09-04 08:38:10 -05:00
Jason DoveandGitHub 82de3136cd fix hls session worker lifetime (#1414) 2023-09-04 08:30:01 -05:00
Jason DoveandGitHub b1cd324f9c fix docker builds (#1413) 2023-09-03 08:54:48 -05:00
Jason DoveandGitHub 5caf8f7f98 fix builds (#1412) 2023-09-03 08:52:00 -05:00
Jason DoveandGitHub 245c4ec359 code analysis and cleanup (#1411)
* cleanup scanner project

* cleanup infrastructure projects

* cleanup ffmpeg project

* cleanup core project

* cleanup app project

* cleanup main project

* update dependencies

* code cleanup
2023-09-03 06:23:42 -05:00
Jason DoveandGitHub 6414471ace fix emby movie libraries (#1410) 2023-09-02 13:03:48 -05:00
Jason DoveandGitHub 8b0b927a5c use d3d11va for qsv accel on windows (#1408) 2023-09-01 11:50:56 -05:00
Jason DoveandGitHub e5962699a4 schedule item editor updates (#1407) 2023-09-01 06:44:31 -05:00
Jason DoveandGitHub 27504e42bc fix mysql show queries (#1406)
* ignore artwork when sync is disabled

* fix delete playout

* fix some mysql queries
2023-09-01 06:05:36 -05:00
Jason DoveandGitHub deb0ac49b5 show plex server names in libraries page (#1402)
* cleanup plex libraries query

* show plex server names in libraries page
2023-08-29 08:46:36 -05:00
Jason DoveandGitHub 225b95449c rework hls session state (#1401) 2023-08-29 05:59:35 -05:00
Jason DoveandGitHub cb43c28d00 fix hls session when starting with very short content (#1400) 2023-08-29 05:01:13 -05:00
Jason DoveandGitHub 0a75136223 fix transcoding short content (#1399) 2023-08-28 22:01:38 -05:00
Jason Dove efc710749e fix test 2023-08-28 20:44:35 -05:00
Jason DoveandGitHub 8f241f49fc optimize transcoding speed (#1398)
* fix "empty trash" button blinking when loading trash page

* clean channel guide cache on startup

* only work-ahead in hls session for 2 minutes
2023-08-28 20:42:02 -05:00
Jason DoveandGitHub 20d224fcfd more mysql fixes (#1397)
* fix channels and movie page crashes with mysql

* update dependencies
2023-08-26 10:39:33 -05:00
Jason DoveandGitHub b3fda4e88d allow shared plex servers (#1391)
* allow shared plex servers

* update dependencies
2023-08-22 08:49:26 -05:00
Jason DoveandGitHub 560cb826b3 fix local subtitles display (#1388)
* show external subtitles in media info

* fix mysql saved page size
2023-08-19 11:26:52 -05:00
Jason DoveandGitHub b038a58fa2 fix saving smart collection (#1384) 2023-08-15 15:53:56 -05:00
Jason DoveandGitHub c28e201e47 fix adding shows directly to schedule (#1383) 2023-08-15 15:34:53 -05:00
Jason DoveandGitHub b84bb6b437 fix ui crashes (#1382)
* fix ffmpeg editor crash

* fix watermark editor

* fix multi collection editor

* fix filler preset editor crash
2023-08-15 06:39:29 -05:00
Jason DoveandGitHub 641b8bcd10 fix windows dependency (#1380) 2023-08-14 19:44:18 -05:00
Jason DoveandGitHub 4b08ed5381 fix mysql migration (initial data) (#1379) 2023-08-14 15:21:42 -05:00
Jason Dove 5486dcdcab remove docs files and workflow [no ci] 2023-08-13 20:16:49 -05:00
Jason DoveandGitHub 77b32b0f09 fix github repo links (#1378) 2023-08-13 09:59:48 -05:00
Jason Dove 2c6c08becf update submodule [no ci] 2023-08-13 09:33:33 -05:00
Jason DoveandGitHub 99bc19cf26 fix docker builds (#1376) 2023-08-12 22:02:45 -05:00
Jason DoveandGitHub a7661c8498 add mysql database provider (#1375)
* refactor sqlite into separate library

* support mysql

* fixes

* sql fixes

* cleanup

* update changelog
2023-08-12 21:44:14 -05:00
Jason DoveandGitHub d951035183 fix bulk removing items from elasticsearch index (#1374) 2023-08-12 08:51:12 -05:00
Jason DoveandGitHub 097c60169c elasticsearch relative queries (#1373)
* remove unused code

* fix relative queries with elasticsearch

* fix some double page loads

* simplify language model
2023-08-12 06:36:28 -05:00
Jason DoveandGitHub d64d8b0454 don't always rebuild elasticsearch index on startup (#1372) 2023-08-11 15:47:17 -05:00
Jason DoveandGitHub 7486304ed9 fix elasticsearch smart collection playouts (#1371) 2023-08-11 15:28:57 -05:00
Jason DoveandGitHub c62cc98c9f add elasticsearch search index provider (#1370)
* wip

* first pass at elasticsearch; movies kind of work

* use field name constants

* properly sort search results

* fix some crashes

* fix page map/jump letters

* optimize page map using terms aggregation

* index all item types

* optionally use elastic search

* code cleanup

* automatically rebuild lucene index after improper shutdown

* update changelog
2023-08-11 13:57:50 -05:00
Jason Dove 22a13cb1b3 Revert "add debug logs for other video folder scanning (#1369)"
This reverts commit 5e573461f3.
2023-08-10 12:51:47 -05:00
Jason DoveandGitHub 5e573461f3 add debug logs for other video folder scanning (#1369) 2023-08-10 11:26:48 -05:00
Jason DoveandGitHub 76c596a7d8 fix logs page (#1368)
* fix log viewer crash

* update dependencies
2023-08-10 10:45:43 -05:00
Jason DoveandGitHub f945f16d97 fix qsv subtitle scaling (#1367) 2023-08-10 10:16:10 -05:00
Jason DoveandGitHub 797d4005e2 replace moq with nsubstitute (#1365) 2023-08-09 20:14:18 -05:00
Jason Dove 55903430ae update changelog for release v0.8.1-beta [no ci] 2023-08-07 13:32:07 -05:00
Jason DoveandGitHub f929dc92d1 update dependencies; code cleanup (#1357)
* update dependencies

* code cleanup
2023-08-07 09:34:25 -05:00
Jason DoveandGitHub 2ad27c2be0 update dependencies (#1348)
* update dependencies

* silence mudblazor debug logs
2023-07-24 20:40:32 -05:00
Jason DoveandGitHub df2db5caf7 add plex file name logging (#1342) 2023-07-12 19:27:16 -05:00
Jason DoveandGitHub 5978e8ecb1 fix vaapi rate control mode (#1340) 2023-07-08 12:36:07 -05:00
Jason Dove a540efc2e1 add community to readme [no ci] 2023-07-03 13:07:42 -05:00
Jason DoveandGitHub 1938cef6ae add community link to docs (#1339) 2023-07-03 13:01:54 -05:00
Jason DoveandGitHub b23d798aff update dependencies (#1329) 2023-06-26 11:11:40 -05:00
Jason DoveandGitHub ebad7664b0 force hw accel to use one thread (#1327) 2023-06-25 09:56:58 -05:00
Jason DoveandGitHub a9c93ff498 add custom resolution management (#1326)
* update some dependencies

* add custom resolution management
2023-06-25 09:14:19 -05:00
Jason DoveandGitHub 8277894f7b show database and search index initialization in ui (#1325)
* unblock startup, show database initialization message

* wait on search index to be ready (rebuild)

* clean logging and fake delay
2023-06-24 09:12:46 -05:00
Jason DoveandGitHub 0d66f752b6 add global mutex to ensure single instance (#1324) 2023-06-24 06:30:55 -05:00
1138 changed files with 81566 additions and 6491 deletions
+1 -7
View File
@@ -3,16 +3,10 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2023.1.1",
"version": "2023.2.0",
"commands": [
"jb"
]
},
"swashbuckle.aspnetcore.cli": {
"version": "5.6.2",
"commands": [
"swagger"
]
}
}
}
+5
View File
@@ -83,3 +83,8 @@ tab_width=4
[*.yml]
indent_style = space
indent_size = 2
[*.cs]
# disable CA1848: Use the LoggerMessage delegates`
dotnet_diagnostic.ca1848.severity = none
+6 -12
View File
@@ -41,7 +41,7 @@ jobs:
target: osx-arm64
steps:
- name: Get the sources
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
@@ -118,12 +118,8 @@ jobs:
- name: Notarize
shell: bash
run: |
brew tap mitchellh/gon
brew install mitchellh/gon/gon
gon -log-level=debug -log-json ./gon.json
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
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
@@ -172,7 +168,7 @@ jobs:
target: win-x64
steps:
- name: Get the sources
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -208,7 +204,7 @@ jobs:
id: downloadffmpeg
name: Download ffmpeg
with:
url: "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.7z"
url: "https://github.com/ErsatzTV/ErsatzTV-ffmpeg/releases/download/6.1-working-cuvid/ffmpeg-6.1-working-cuvid.7z"
target: ffmpeg/
- name: Build
@@ -245,9 +241,6 @@ jobs:
# Delete output directory
rm -r "$release_name"
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
@@ -259,6 +252,7 @@ jobs:
assets: |
*${{ matrix.target }}.zip
*${{ matrix.target }}.tar.gz
- name: Publish
uses: softprops/action-gh-release@v1
with:
+3 -3
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Get the sources
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract Docker Tag
@@ -34,7 +34,7 @@ jobs:
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
info_version: ${{ env.INFO_VERSION }}
build_and_upload:
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
uses: ersatztv/ersatztv/.github/workflows/artifacts.yml@main
needs: calculate_version
with:
release_tag: develop
@@ -47,7 +47,7 @@ jobs:
ac_password: ${{ secrets.AC_PASSWORD }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
needs: calculate_version
with:
base_version: develop
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
qemu: true
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
-21
View File
@@ -1,21 +0,0 @@
name: Publish docs via GitHub Pages
on:
push:
branches:
- main
paths:
- docs/**
- mkdocs.yml
jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v3
- name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CUSTOM_DOMAIN: ersatztv.org
+3 -3
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Get the sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
@@ -41,7 +41,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Get the sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
@@ -66,7 +66,7 @@ jobs:
runs-on: macos-11
steps:
- name: Get the sources
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
+3 -3
View File
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Get the sources
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract Docker Tag
@@ -29,7 +29,7 @@ jobs:
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
info_version: ${{ env.INFO_VERSION }}
build_and_upload:
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
uses: ersatztv/ersatztv/.github/workflows/artifacts.yml@main
needs: calculate_version
with:
release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
@@ -42,7 +42,7 @@ jobs:
ac_password: ${{ secrets.AC_PASSWORD }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
needs: calculate_version
with:
base_version: latest
+4 -4
View File
@@ -7,12 +7,12 @@ jobs:
steps:
# Checkout the current repo
- name: Checkout current repository
uses: actions/checkout@v3
# Setup NodeJS version 14
- name: Setup NodeJS V14.x.x
uses: actions/checkout@v4
# Setup NodeJS version 16
- name: Setup NodeJS V16.x.x
uses: actions/setup-node@v3
with:
node-version: '14'
node-version: '16'
# CD into the current client directory and lint and build the client
- name: Lint and Build the client
run: |
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "ErsatzTV-macOS"]
path = ErsatzTV-macOS
url = git@github.com:jasongdove/ErsatzTV-macOS.git
url = git@github.com:ErsatzTV/ErsatzTV-macOS.git
+229 -119
View File
@@ -5,6 +5,113 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [0.8.3-beta] - 2023-11-22
### Added
- Add `Scaling Behavior` option to FFmpeg Profile
- `Scale and Pad`: the default behavior and will maintain aspect ratio of all content
- `Stretch`: a new mode that will NOT maintain aspect ratio when normalizing source content to the desired resolution
- `Crop`: a new mode that will scale beyond the desired resolution (maintaining aspect ratio), and crop to desired resolution
- **This mode does NOT detect black and intelligently crop**
- The goal is to fill the canvas by over-scaling and cropping, instead of minimally scaling and padding
- Include `inputstream.ffmpegdirect` properties in channels.m3u when requested by Kodi
- Log playout item title and path when starting a stream
- This will help with media server libraries where the URL passed to ffmpeg doesn't indicate which file is streaming
- Add QSV Capabilities to Troubleshooting page
- Add `language_tag` and `seconds` fields to search index
- Allow synchronizing Plex `TV Show` libraries that use `Personal Media Shows` agent
- Include Noto CJK Fonts in docker images to support those characters in generated subtitles like songs and music video credits
- Support show fallback metadata with folder names like `Show.Name(1992)`
### Fixed
- Fix playout bug that caused some schedule items with fixed start times to be pushed to the next day
- Fix playout bug that prevented padded durations from fitting within a schedule item of the same duration
- For example, filler that padded to 30 minutes would often not fit in a 30 minute duration schedule item
- Fix VAAPI transcoding 8-bit source content to 10-bit
- Fix NVIDIA subtitle scaling when `scale_npp` filter is unavailable
- Remove ffmpeg and ffprobe as required dependencies for scanning media server libraries
- Note that ffmpeg is still *always* required for playback to work
- Fix PGS subtitle pixel format with Intel VAAPI
- Fix some cases where `Copy` button would fail to copy to clipboard
- Fix some cases where ffmpeg process would remain running after properly closing ErsatzTV
- Fix QSV HLS segment duration
- This behavior caused extremely slow QSV stream starts
- Fix displaying multiple languages in UI for movies, artists, shows
- Fix MySQL queries that could fail during media server library scans
- Fix scanning Jellyfin libraries when library options and/or path infos are not returned from Jellyfin API
- Fix error indexing music videos in `File Not Found` state
- Fix bug scheduling duration filler when filler collection contains item with zero duration
- Fix bug displaying television seasons for shows that have no year metadata
### Changed
- Upgrade ffmpeg to 6.1, which is now *required* for all installs
- Use new ffmpeg throttling method to minimize cpu/gpu use without impacting audio normalization
- Change FFmpeg Profile `Normalize Loudness` setting from checkbox to dropdown
- `Off`: do not normalize loudness
- `loudnorm`: use `loudnorm` filter to normalize loudness (generally higher CPU use)
- `dynaudnorm`: use `dynaudnorm` filter to normalize loudness (generally lower CPU use)
- Jellyfin collection scanning will no longer happen after every (automatic or forced) library scan
- Automatic/periodic scans will check collections one time after all libraries have been scanned
- There is a new table in the `Media` > `Libraries` page with a button to manually re-scan Jellyfin collections as needed
- In FFmpeg Profile editor, only display hardware acceleration kinds that are supported by the configured ffmpeg
- Test QSV acceleration if configured, and fallback to software mode if test fails
- Detect QSV capabilities on Linux (supported decoders, encoders)
- Use hardware acceleration for error messages/offline messages
- Try to parse season number from season folder when Jellyfin does not provide season number
- This *may* fix issues where Jellyfin libraries show all season numbers as 0 (specials)
- Rework Plex collection scanning
- Automatic/periodic scans will check collections one time after all libraries have been scanned
- There is a table in the `Media` > `Libraries` page with a button to manually re-scan Plex collections as needed
- Plex smart collections will now be synchronized as tags, similar to other Plex collections
## [0.8.2-beta] - 2023-09-14
### Added
- Automatically rebuild search index after improper shutdown
- Add *experimental* support for Elasticsearch as search index backend
- No query changes should be needed since ES is backed by lucene and supports the same query syntax
- This can be configured using the following env vars (note the double underscore separator `__`)
- `ELASTICSEARCH__URI` (e.g. `http://localhost:9200`)
- `ELASTICSEARCH__INDEXNAME` (default is `ersatztv`)
- Add *experimental* support for MySQL/MariaDB database provider
- ***There is no functionality to migrate data between providers***
- This can be configured using the following env vars (note the double underscore separator `__`)
- `PROVIDER` - set to `MySql`
- `MYSQL__CONNECTIONSTRING` - (e.g. `Server=localhost;Database=ErsatzTV;Uid=root;Pwd=ersatztv;`)
- Add option to use shared Plex servers, not just owned servers
- This can be enabled by setting the env var `ETV_ALLOW_SHARED_PLEX_SERVERS` to any non-empty value
- Show Plex server names in Libraries page
### Fixed
- Fix subtitle scaling when using QSV hardware acceleration
- Fix log viewer crash when log file contains invalid data
- Clean channel guide cache on startup (delete channels that no longer exist)
- Fix Emby movie libraries so local file access is not required
- Fix adding alternate schedule
- Fix parsing show title from NFO file that also contains season information
### Changed
- Optimize transcoding session to only work ahead (at max speed) for 3 minutes before throttling to realtime
- This should *greatly* reduce cpu/gpu use when joining a channel, particularly with long content
- Allow manually editing (typing) schedule item fixed start time
- Use different control for editing schedule item duration, and allow 24-hour duration
- This is needed if you want a default/fallback alternate schedule to fill the entire day with one schedule item
- The schedule item should have a fixed start time of midnight (00:00) and a duration of 24 hours
- Use Direct3D 11 for QSV acceleration on Windows
## [0.8.1-beta] - 2023-08-07
### Added
- Add custom resolution management to `Settings` page
### Fixed
- Only allow a single instance of ErsatzTV to run
- This fixes some cases where the search index would become unusable
- Fix VAAPI rate control mode capability check
### Changed
- Rework startup process to show UI as early as possible
- A minimal UI will indicate when the database and search index are initializing
- The UI will automatically refresh when the initialization processes have completed
- Force ffmpeg to use one thread when hardware acceleration is used since hardware acceleration does not support multiple threads
## [0.8.0-beta] - 2023-06-23
### Added
- Disable playout buttons and show spinning indicator when a playout is being modified (built/extended, or subtitles are being extracted)
@@ -152,7 +259,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Use VP9 hardware-accelerated decoder with VAAPI when available
### Fixed
- Align default docker image (no acceleration) with new images from [ErsatzTV-ffmpeg](https://github.com/jasongdove/ErsatzTV-ffmpeg)
- Align default docker image (no acceleration) with new images from [ErsatzTV-ffmpeg](https://github.com/ErsatzTV/ErsatzTV-ffmpeg)
- Fix some transcoding pipelines that use software decoders
- Improve VAAPI encoder capability detection on newer hardware
- Fix trash page to properly display episodes with missing metadata or titles
@@ -1692,121 +1799,124 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.8.0-beta...HEAD
[0.8.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.9-beta...v0.8.0-beta
[0.7.9-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.8-beta...v0.7.9-beta
[0.7.8-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.7-beta...v0.7.8-beta
[0.7.7-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.6-beta...v0.7.7-beta
[0.7.6-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.5-beta...v0.7.6-beta
[0.7.5-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.4-beta...v0.7.5-beta
[0.7.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.3-beta...v0.7.4-beta
[0.7.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.2-beta...v0.7.3-beta
[0.7.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.1-beta...v0.7.2-beta
[0.7.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.0-beta...v0.7.1-beta
[0.7.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.9-beta...v0.7.0-beta
[0.6.9-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.8-beta...v0.6.9-beta
[0.6.8-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.7-beta...v0.6.8-beta
[0.6.7-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.6-beta...v0.6.7-beta
[0.6.6-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.5-beta...v0.6.6-beta
[0.6.5-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.4-beta...v0.6.5-beta
[0.6.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.3-beta...v0.6.4-beta
[0.6.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.2-beta...v0.6.3-beta
[0.6.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.1-beta...v0.6.2-beta
[0.6.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.0-beta...v0.6.1-beta
[0.6.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.8-beta...v0.6.0-beta
[0.5.8-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.7-beta...v0.5.8-beta
[0.5.7-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.6-beta...v0.5.7-beta
[0.5.6-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.5-beta...v0.5.6-beta
[0.5.5-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.4-beta...v0.5.5-beta
[0.5.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.3-beta...v0.5.4-beta
[0.5.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.2-beta...v0.5.3-beta
[0.5.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.1-beta...v0.5.2-beta
[0.5.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.0-beta...v0.5.1-beta
[0.5.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.5-alpha...v0.5.0-beta
[0.4.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.4-alpha...v0.4.5-alpha
[0.4.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.3-alpha...v0.4.4-alpha
[0.4.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.2-alpha...v0.4.3-alpha
[0.4.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.1-alpha...v0.4.2-alpha
[0.4.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.0-alpha...v0.4.1-alpha
[0.4.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.8-alpha...v0.4.0-alpha
[0.3.8-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.7-alpha...v0.3.8-alpha
[0.3.7-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...v0.3.7-alpha
[0.3.6-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha
[0.3.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha
[0.3.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha
[0.3.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.1-alpha...v0.3.2-alpha
[0.3.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.0-alpha...v0.3.1-alpha
[0.3.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.5-alpha...v0.3.0-alpha
[0.2.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.4-alpha...v0.2.5-alpha
[0.2.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.3-alpha...v0.2.4-alpha
[0.2.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.2-alpha...v0.2.3-alpha
[0.2.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.1-alpha...v0.2.2-alpha
[0.2.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.0-alpha...v0.2.1-alpha
[0.2.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.5-alpha...v0.2.0-alpha
[0.1.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.4-alpha...v0.1.5-alpha
[0.1.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.3-alpha...v0.1.4-alpha
[0.1.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.2-alpha...v0.1.3-alpha
[0.1.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.1-alpha...v0.1.2-alpha
[0.1.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.0-alpha...v0.1.1-alpha
[0.1.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.62-alpha...v0.1.0-alpha
[0.0.62-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.61-alpha...v0.0.62-alpha
[0.0.61-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.60-alpha...v0.0.61-alpha
[0.0.60-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.59-alpha...v0.0.60-alpha
[0.0.59-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.58-alpha...v0.0.59-alpha
[0.0.58-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.57-alpha...v0.0.58-alpha
[0.0.57-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.56-alpha...v0.0.57-alpha
[0.0.56-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.55-alpha...v0.0.56-alpha
[0.0.55-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.54-alpha...v0.0.55-alpha
[0.0.54-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.53-alpha...v0.0.54-alpha
[0.0.53-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.52-alpha...v0.0.53-alpha
[0.0.52-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.51-alpha...v0.0.52-alpha
[0.0.51-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.50-alpha...v0.0.51-alpha
[0.0.50-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.49-prealpha...v0.0.50-alpha
[0.0.49-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.48-prealpha...v0.0.49-prealpha
[0.0.48-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.47-prealpha...v0.0.48-prealpha
[0.0.47-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.46-prealpha...v0.0.47-prealpha
[0.0.46-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.45-prealpha...v0.0.46-prealpha
[0.0.45-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.44-prealpha...v0.0.45-prealpha
[0.0.44-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.43-prealpha...v0.0.44-prealpha
[0.0.43-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.42-prealpha...v0.0.43-prealpha
[0.0.42-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.41-prealpha...v0.0.42-prealpha
[0.0.41-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.40-prealpha...v0.0.41-prealpha
[0.0.40-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.39-prealpha...v0.0.40-prealpha
[0.0.39-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.38-prealpha...v0.0.39-prealpha
[0.0.38-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.37-prealpha...v0.0.38-prealpha
[0.0.37-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.36-prealpha...v0.0.37-prealpha
[0.0.36-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.35-prealpha...v0.0.36-prealpha
[0.0.35-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.34-prealpha...v0.0.35-prealpha
[0.0.34-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.33-prealpha...v0.0.34-prealpha
[0.0.33-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.32-prealpha...v0.0.33-prealpha
[0.0.32-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.31-prealpha...v0.0.32-prealpha
[0.0.31-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.29-prealpha...v0.0.31-prealpha
[0.0.29-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.28-prealpha...v0.0.29-prealpha
[0.0.28-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.27-prealpha...v0.0.28-prealpha
[0.0.27-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.26-prealpha...v0.0.27-prealpha
[0.0.26-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.25-prealpha...v0.0.26-prealpha
[0.0.25-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.24-prealpha...v0.0.25-prealpha
[0.0.24-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.23-prealpha...v0.0.24-prealpha
[0.0.23-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.22-prealpha...v0.0.23-prealpha
[0.0.22-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.21-prealpha...v0.0.22-prealpha
[0.0.21-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.20-prealpha...v0.0.21-prealpha
[0.0.20-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.19-prealpha...v0.0.20-prealpha
[0.0.19-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.18-prealpha...v0.0.19-prealpha
[0.0.18-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.17-prealpha...v0.0.18-prealpha
[0.0.17-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.16-prealpha...v0.0.17-prealpha
[0.0.16-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.15-prealpha...v0.0.16-prealpha
[0.0.15-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.14-prealpha...v0.0.15-prealpha
[0.0.14-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.13-prealpha...v0.0.14-prealpha
[0.0.13-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.12-prealpha...v0.0.13-prealpha
[0.0.12-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.11-prealpha...v0.0.12-prealpha
[0.0.11-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.10-prealpha...v0.0.11-prealpha
[0.0.10-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.9-prealpha...v0.0.10-prealpha
[0.0.9-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.8-prealpha...v0.0.9-prealpha
[0.0.8-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.7-prealpha...v0.0.8-prealpha
[0.0.7-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.6-prealpha...v0.0.7-prealpha
[0.0.6-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.5-prealpha...v0.0.6-prealpha
[0.0.5-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.4-prealpha...v0.0.5-prealpha
[0.0.4-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.3-prealpha...v0.0.4-prealpha
[0.0.3-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.1-prealpha...v0.0.3-prealpha
[0.0.1-prealpha]: https://github.com/jasongdove/ErsatzTV/releases/tag/v0.0.1-prealpha
[Unreleased]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.3-beta...HEAD
[0.8.3-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.2-beta...v0.8.3-beta
[0.8.2-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.1-beta...v0.8.2-beta
[0.8.1-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.0-beta...v0.8.1-beta
[0.8.0-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.9-beta...v0.8.0-beta
[0.7.9-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.8-beta...v0.7.9-beta
[0.7.8-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.7-beta...v0.7.8-beta
[0.7.7-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.6-beta...v0.7.7-beta
[0.7.6-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.5-beta...v0.7.6-beta
[0.7.5-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.4-beta...v0.7.5-beta
[0.7.4-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.3-beta...v0.7.4-beta
[0.7.3-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.2-beta...v0.7.3-beta
[0.7.2-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.1-beta...v0.7.2-beta
[0.7.1-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.0-beta...v0.7.1-beta
[0.7.0-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.9-beta...v0.7.0-beta
[0.6.9-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.8-beta...v0.6.9-beta
[0.6.8-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.7-beta...v0.6.8-beta
[0.6.7-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.6-beta...v0.6.7-beta
[0.6.6-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.5-beta...v0.6.6-beta
[0.6.5-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.4-beta...v0.6.5-beta
[0.6.4-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.3-beta...v0.6.4-beta
[0.6.3-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.2-beta...v0.6.3-beta
[0.6.2-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.1-beta...v0.6.2-beta
[0.6.1-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.6.0-beta...v0.6.1-beta
[0.6.0-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.5.8-beta...v0.6.0-beta
[0.5.8-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.5.7-beta...v0.5.8-beta
[0.5.7-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.5.6-beta...v0.5.7-beta
[0.5.6-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.5.5-beta...v0.5.6-beta
[0.5.5-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.5.4-beta...v0.5.5-beta
[0.5.4-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.5.3-beta...v0.5.4-beta
[0.5.3-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.5.2-beta...v0.5.3-beta
[0.5.2-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.5.1-beta...v0.5.2-beta
[0.5.1-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.5.0-beta...v0.5.1-beta
[0.5.0-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.4.5-alpha...v0.5.0-beta
[0.4.5-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.4.4-alpha...v0.4.5-alpha
[0.4.4-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.4.3-alpha...v0.4.4-alpha
[0.4.3-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.4.2-alpha...v0.4.3-alpha
[0.4.2-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.4.1-alpha...v0.4.2-alpha
[0.4.1-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.4.0-alpha...v0.4.1-alpha
[0.4.0-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.3.8-alpha...v0.4.0-alpha
[0.3.8-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.3.7-alpha...v0.3.8-alpha
[0.3.7-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.3.6-alpha...v0.3.7-alpha
[0.3.6-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha
[0.3.5-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha
[0.3.4-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha
[0.3.3-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha
[0.3.2-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.3.1-alpha...v0.3.2-alpha
[0.3.1-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.3.0-alpha...v0.3.1-alpha
[0.3.0-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.2.5-alpha...v0.3.0-alpha
[0.2.5-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.2.4-alpha...v0.2.5-alpha
[0.2.4-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.2.3-alpha...v0.2.4-alpha
[0.2.3-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.2.2-alpha...v0.2.3-alpha
[0.2.2-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.2.1-alpha...v0.2.2-alpha
[0.2.1-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.2.0-alpha...v0.2.1-alpha
[0.2.0-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.1.5-alpha...v0.2.0-alpha
[0.1.5-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.1.4-alpha...v0.1.5-alpha
[0.1.4-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.1.3-alpha...v0.1.4-alpha
[0.1.3-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.1.2-alpha...v0.1.3-alpha
[0.1.2-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.1.1-alpha...v0.1.2-alpha
[0.1.1-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.1.0-alpha...v0.1.1-alpha
[0.1.0-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.62-alpha...v0.1.0-alpha
[0.0.62-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.61-alpha...v0.0.62-alpha
[0.0.61-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.60-alpha...v0.0.61-alpha
[0.0.60-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.59-alpha...v0.0.60-alpha
[0.0.59-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.58-alpha...v0.0.59-alpha
[0.0.58-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.57-alpha...v0.0.58-alpha
[0.0.57-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.56-alpha...v0.0.57-alpha
[0.0.56-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.55-alpha...v0.0.56-alpha
[0.0.55-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.54-alpha...v0.0.55-alpha
[0.0.54-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.53-alpha...v0.0.54-alpha
[0.0.53-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.52-alpha...v0.0.53-alpha
[0.0.52-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.51-alpha...v0.0.52-alpha
[0.0.51-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.50-alpha...v0.0.51-alpha
[0.0.50-alpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.49-prealpha...v0.0.50-alpha
[0.0.49-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.48-prealpha...v0.0.49-prealpha
[0.0.48-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.47-prealpha...v0.0.48-prealpha
[0.0.47-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.46-prealpha...v0.0.47-prealpha
[0.0.46-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.45-prealpha...v0.0.46-prealpha
[0.0.45-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.44-prealpha...v0.0.45-prealpha
[0.0.44-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.43-prealpha...v0.0.44-prealpha
[0.0.43-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.42-prealpha...v0.0.43-prealpha
[0.0.42-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.41-prealpha...v0.0.42-prealpha
[0.0.41-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.40-prealpha...v0.0.41-prealpha
[0.0.40-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.39-prealpha...v0.0.40-prealpha
[0.0.39-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.38-prealpha...v0.0.39-prealpha
[0.0.38-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.37-prealpha...v0.0.38-prealpha
[0.0.37-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.36-prealpha...v0.0.37-prealpha
[0.0.36-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.35-prealpha...v0.0.36-prealpha
[0.0.35-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.34-prealpha...v0.0.35-prealpha
[0.0.34-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.33-prealpha...v0.0.34-prealpha
[0.0.33-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.32-prealpha...v0.0.33-prealpha
[0.0.32-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.31-prealpha...v0.0.32-prealpha
[0.0.31-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.29-prealpha...v0.0.31-prealpha
[0.0.29-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.28-prealpha...v0.0.29-prealpha
[0.0.28-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.27-prealpha...v0.0.28-prealpha
[0.0.27-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.26-prealpha...v0.0.27-prealpha
[0.0.26-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.25-prealpha...v0.0.26-prealpha
[0.0.25-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.24-prealpha...v0.0.25-prealpha
[0.0.24-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.23-prealpha...v0.0.24-prealpha
[0.0.23-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.22-prealpha...v0.0.23-prealpha
[0.0.22-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.21-prealpha...v0.0.22-prealpha
[0.0.21-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.20-prealpha...v0.0.21-prealpha
[0.0.20-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.19-prealpha...v0.0.20-prealpha
[0.0.19-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.18-prealpha...v0.0.19-prealpha
[0.0.18-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.17-prealpha...v0.0.18-prealpha
[0.0.17-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.16-prealpha...v0.0.17-prealpha
[0.0.16-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.15-prealpha...v0.0.16-prealpha
[0.0.15-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.14-prealpha...v0.0.15-prealpha
[0.0.14-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.13-prealpha...v0.0.14-prealpha
[0.0.13-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.12-prealpha...v0.0.13-prealpha
[0.0.12-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.11-prealpha...v0.0.12-prealpha
[0.0.11-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.10-prealpha...v0.0.11-prealpha
[0.0.10-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.9-prealpha...v0.0.10-prealpha
[0.0.9-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.8-prealpha...v0.0.9-prealpha
[0.0.8-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.7-prealpha...v0.0.8-prealpha
[0.0.7-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.6-prealpha...v0.0.7-prealpha
[0.0.6-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.5-prealpha...v0.0.6-prealpha
[0.0.5-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.4-prealpha...v0.0.5-prealpha
[0.0.4-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.3-prealpha...v0.0.4-prealpha
[0.0.3-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.1-prealpha...v0.0.3-prealpha
[0.0.1-prealpha]: https://github.com/ErsatzTV/ErsatzTV/releases/tag/v0.0.1-prealpha
+7
View File
@@ -173,6 +173,7 @@ version = "0.1.0"
dependencies = [
"process_path",
"special-folder",
"static_vcruntime",
"tray-item",
"windows",
"windres",
@@ -769,6 +770,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "static_vcruntime"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "954e3e877803def9dc46075bf4060147c55cd70db97873077232eae0269dc89b"
[[package]]
name = "strum"
version = "0.24.1"
+1
View File
@@ -17,3 +17,4 @@ features = [
[build-dependencies]
windres = "*"
static_vcruntime = "2.0"
+1
View File
@@ -1,5 +1,6 @@
use windres::Build;
fn main() {
static_vcruntime::metabuild();
Build::new().compile("ersatztv_windows.rc").unwrap();
}
+5
View File
@@ -0,0 +1,5 @@
[*.cs]
# disable CA1711: Identifiers should not have incorrect suffix
dotnet_diagnostic.ca1711.severity = none
# disable CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.ca1848.severity = none
+1 -2
View File
@@ -29,12 +29,11 @@ internal static class Mapper
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languages
.Distinct()
.Map(
lang => allCultures.Filter(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence()
.Flatten()
.Distinct()
.ToList();
}
}
@@ -35,7 +35,7 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
private async Task<Unit> DoDeletion(TvContext dbContext, Channel channel, CancellationToken cancellationToken)
{
dbContext.Channels.Remove(channel);
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
// delete channel data from channel guide cache
string cacheFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channel.Number}.xml");
@@ -1,3 +1,4 @@
using System.Globalization;
using System.Xml;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@@ -136,10 +137,13 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
PlayoutItem finishItem = sorted[finishIndex];
i = finishIndex;
string start = startItem.StartOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
string start = startItem.StartOffset.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
.Replace(":", string.Empty);
string stop = displayItem.GuideFinishOffset.HasValue
? displayItem.GuideFinishOffset.Value.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty)
: finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
? displayItem.GuideFinishOffset.Value.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
.Replace(":", string.Empty)
: finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
.Replace(":", string.Empty);
string title = GetTitle(displayItem);
string subtitle = GetSubtitle(displayItem);
@@ -182,7 +186,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
if (metadata.Year.HasValue)
{
await xml.WriteStartElementAsync(null, "date", null);
await xml.WriteStringAsync(metadata.Year.Value.ToString());
await xml.WriteStringAsync(metadata.Year.Value.ToString(CultureInfo.InvariantCulture));
await xml.WriteEndElementAsync(); // date
}
@@ -220,7 +224,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
if (metadata.Year.HasValue)
{
await xml.WriteStartElementAsync(null, "date", null);
await xml.WriteStringAsync(metadata.Year.Value.ToString());
await xml.WriteStringAsync(metadata.Year.Value.ToString(CultureInfo.InvariantCulture));
await xml.WriteEndElementAsync(); // date
}
@@ -370,11 +374,11 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
_ => 440
};
if (artworkPath.StartsWith("jellyfin://"))
if (artworkPath.StartsWith("jellyfin://", StringComparison.OrdinalIgnoreCase))
{
artworkPath = JellyfinUrl.PlaceholderProxyForArtwork(artworkPath, artworkKind, height);
}
else if (artworkPath.StartsWith("emby://"))
else if (artworkPath.StartsWith("emby://", StringComparison.OrdinalIgnoreCase))
{
artworkPath = EmbyUrl.PlaceholderProxyForArtwork(artworkPath, artworkKind, height);
}
@@ -499,7 +503,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
}).Flatten();
}
private string GetPrioritizedArtworkPath(Metadata metadata)
private static string GetPrioritizedArtworkPath(Metadata metadata)
{
Option<string> maybeArtwork = Optional(metadata.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Poster)
@@ -517,5 +521,5 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
return maybeArtwork.IfNone(string.Empty);
}
private record ContentRating(Option<string> System, string Value);
private sealed record ContentRating(Option<string> System, string Value);
}
@@ -1,4 +1,3 @@
using System.Data;
using System.Data.Common;
using System.Xml;
using Dapper;
@@ -84,7 +83,8 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
from Channel C
left outer join Artwork A on C.Id = A.ChannelId and A.ArtworkKind = 2
where C.Id in (select ChannelId from Playout)
order by CAST(C.Number as real)";
order by CAST(C.Number as double)";
// TODO: this needs to be fixed for sqlite/mariadb
await using var reader = (DbDataReader)await dbContext.Connection.ExecuteReaderAsync(QUERY);
Func<DbDataReader, ChannelResult> rowParser = reader.GetRowParser<ChannelResult>();
@@ -112,5 +112,5 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
: $"{{RequestBase}}/iptv/logos/{channel.ArtworkPath}.jpg{{AccessTokenUri}}";
// ReSharper disable once ClassNeverInstantiated.Local
private record ChannelResult(string Number, string Name, string Categories, string ArtworkPath);
private sealed record ChannelResult(string Number, string Name, string Categories, string ArtworkPath);
}
@@ -92,7 +92,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
return ProjectToViewModel(c);
}
private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
(await ChannelMustExist(dbContext, request), ValidateName(request),
await ValidateNumber(dbContext, request),
ValidatePreferredAudioLanguage(request))
+1 -1
View File
@@ -45,6 +45,6 @@ internal static class Mapper
StreamingMode.TransportStreamHybrid => "MPEG-TS",
StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
_ => throw new ArgumentOutOfRangeException()
_ => throw new ArgumentOutOfRangeException(nameof(channel))
};
}
@@ -10,6 +10,6 @@ public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<Chan
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
_channelRepository.Get(request.Id)
_channelRepository.GetChannel(request.Id)
.MapT(ProjectToViewModel);
}
@@ -3,4 +3,4 @@ using ErsatzTV.Core.Iptv;
namespace ErsatzTV.Application.Channels;
public record GetChannelPlaylist
(string Scheme, string Host, string BaseUrl, string Mode, string AccessToken) : IRequest<ChannelPlaylist>;
(string Scheme, string Host, string BaseUrl, string Mode, string UserAgent, string AccessToken) : IRequest<ChannelPlaylist>;
@@ -20,6 +20,7 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
request.Host,
request.BaseUrl,
channels,
request.UserAgent,
request.AccessToken));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
@@ -1,4 +1,5 @@
using System.Threading.Channels;
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@@ -45,7 +46,8 @@ public class UpdatePlayoutSettingsHandler : IRequestHandler<UpdatePlayoutSetting
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Channel)
.ToListAsync();
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number, CultureInfo.InvariantCulture))
.Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Continue));
}
@@ -13,5 +13,5 @@ public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKe
public Task<Option<ConfigElementViewModel>> Handle(
GetConfigElementByKey request,
CancellationToken cancellationToken) =>
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
_configElementRepository.GetConfigElement(request.Key).MapT(ProjectToViewModel);
}
@@ -1,3 +1,4 @@
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core;
@@ -69,7 +70,7 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
{
var arguments = new List<string>
{
"scan-emby-collections", request.EmbyMediaSourceId.ToString()
"scan-emby-collections", request.EmbyMediaSourceId.ToString(CultureInfo.InvariantCulture)
};
if (request.ForceScan)
@@ -1,4 +1,5 @@
using System.Threading.Channels;
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core;
using ErsatzTV.Core.Errors;
@@ -58,7 +59,7 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
{
var arguments = new List<string>
{
"scan-emby", request.EmbyLibraryId.ToString()
"scan-emby", request.EmbyLibraryId.ToString(CultureInfo.InvariantCulture)
};
if (request.ForceScan)
@@ -52,5 +52,5 @@ public class SaveEmbySecretsHandler : IRequestHandler<SaveEmbySecrets, Either<Ba
return Unit.Default;
}
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
private sealed record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
}
@@ -102,9 +102,7 @@ public class SynchronizeEmbyLibrariesHandler : IRequestHandler<SynchronizeEmbyLi
return Unit.Default;
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
private sealed record ConnectionParameters(EmbyMediaSource EmbyMediaSource, EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
@@ -9,4 +9,4 @@ public record EmbyLibraryViewModel(
LibraryMediaKind MediaKind,
bool ShouldSyncItems,
int MediaSourceId)
: LibraryViewModel("Emby", Id, Name, MediaKind, MediaSourceId);
: LibraryViewModel("Emby", Id, Name, MediaKind, MediaSourceId, string.Empty);
@@ -76,7 +76,7 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
.ToValidation<BaseError>("Emby media source requires an api key");
}
private record ConnectionParameters(
private sealed record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
@@ -4,21 +4,24 @@
<TargetFramework>net7.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.3" />
<PackageReference Include="CliWrap" Version="3.6.4" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="12.0.1" />
<PackageReference Include="MediatR" Version="12.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.6.40">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.8.14">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="2.0.0" />
<PackageReference Include="Winista.MimeDetect" Version="1.0.1" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
@@ -33,6 +33,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plex_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Cqueries/@EntryIndexedValue">True</s:Boolean>
@@ -23,10 +23,10 @@ public class
_ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name)
.Map(ProjectToViewModel);
private Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
private static Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
ValidateName(request).AsTask().MapT(_ => request);
private Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
private static Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
request.NotEmpty(x => x.Name)
.Bind(_ => request.NotLongerThan(50)(x => x.Name));
}
@@ -12,6 +12,7 @@ public record CreateFFmpegProfile(
string VaapiDevice,
int? QsvExtraHardwareFrames,
int ResolutionId,
ScalingBehavior ScalingBehavior,
FFmpegProfileVideoFormat VideoFormat,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
@@ -19,7 +20,7 @@ public record CreateFFmpegProfile(
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
NormalizeLoudnessMode NormalizeLoudnessMode,
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
@@ -32,7 +32,7 @@ public class CreateFFmpegProfileHandler :
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
}
private async Task<Validation<BaseError, FFmpegProfile>> Validate(
private static async Task<Validation<BaseError, FFmpegProfile>> Validate(
TvContext dbContext,
CreateFFmpegProfile request) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
@@ -46,6 +46,7 @@ public class CreateFFmpegProfileHandler :
VaapiDevice = request.VaapiDevice,
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId,
ScalingBehavior = request.ScalingBehavior,
VideoFormat = request.VideoFormat,
BitDepth = request.BitDepth,
VideoBitrate = request.VideoBitrate,
@@ -53,7 +54,7 @@ public class CreateFFmpegProfileHandler :
AudioFormat = request.AudioFormat,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudness = request.NormalizeLoudness,
NormalizeLoudnessMode = request.NormalizeLoudnessMode,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeFramerate = request.NormalizeFramerate,
@@ -13,6 +13,7 @@ public record UpdateFFmpegProfile(
string VaapiDevice,
int? QsvExtraHardwareFrames,
int ResolutionId,
ScalingBehavior ScalingBehavior,
FFmpegProfileVideoFormat VideoFormat,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
@@ -20,7 +21,7 @@ public record UpdateFFmpegProfile(
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
NormalizeLoudnessMode NormalizeLoudnessMode,
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
@@ -20,10 +20,10 @@ public class
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request));
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
}
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
private static async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
TvContext dbContext,
FFmpegProfile p,
UpdateFFmpegProfile update)
@@ -35,6 +35,7 @@ public class
p.VaapiDevice = update.VaapiDevice;
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
p.ResolutionId = update.ResolutionId;
p.ScalingBehavior = update.ScalingBehavior;
p.VideoFormat = update.VideoFormat;
// mpeg2video only supports 8-bit content
@@ -47,7 +48,7 @@ public class
p.AudioFormat = update.AudioFormat;
p.AudioBitrate = update.AudioBitrate;
p.AudioBufferSize = update.AudioBufferSize;
p.NormalizeLoudness = update.NormalizeLoudness;
p.NormalizeLoudnessMode = update.NormalizeLoudnessMode;
p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeFramerate = update.NormalizeFramerate;
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Globalization;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
@@ -52,10 +53,8 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
UseShellExecute = false
};
using var test = new Process
{
StartInfo = startInfo
};
using var test = new Process();
test.StartInfo = startInfo;
test.Start();
string output = await test.StandardOutput.ReadToEndAsync();
@@ -71,7 +70,7 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
await _configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegDefaultProfileId,
request.Settings.DefaultFFmpegProfileId.ToString());
request.Settings.DefaultFFmpegProfileId.ToString(CultureInfo.InvariantCulture));
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegSaveReports,
request.Settings.SaveReports.ToString());
@@ -13,6 +13,7 @@ public record FFmpegProfileViewModel(
string VaapiDevice,
int? QsvExtraHardwareFrames,
ResolutionViewModel Resolution,
ScalingBehavior ScalingBehavior,
FFmpegProfileVideoFormat VideoFormat,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
@@ -20,7 +21,7 @@ public record FFmpegProfileViewModel(
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
NormalizeLoudnessMode NormalizeLoudnessMode,
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
@@ -1,5 +1,4 @@
using ErsatzTV.Application.Resolutions;
using ErsatzTV.Core.Api.FFmpegProfiles;
using ErsatzTV.Core.Api.FFmpegProfiles;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.FFmpegProfiles;
@@ -15,7 +14,8 @@ internal static class Mapper
profile.VaapiDriver,
profile.VaapiDevice,
profile.QsvExtraHardwareFrames,
Project(profile.Resolution),
Resolutions.Mapper.ProjectToViewModel(profile.Resolution),
profile.ScalingBehavior,
profile.VideoFormat,
profile.BitDepth,
profile.VideoBitrate,
@@ -23,7 +23,7 @@ internal static class Mapper
profile.AudioFormat,
profile.AudioBitrate,
profile.AudioBufferSize,
profile.NormalizeLoudness,
profile.NormalizeLoudnessMode,
profile.AudioChannels,
profile.AudioSampleRate,
profile.NormalizeFramerate,
@@ -52,12 +52,9 @@ internal static class Mapper
(int)ffmpegProfile.AudioFormat,
ffmpegProfile.AudioBitrate,
ffmpegProfile.AudioBufferSize,
ffmpegProfile.NormalizeLoudness,
(int)ffmpegProfile.NormalizeLoudnessMode,
ffmpegProfile.AudioChannels,
ffmpegProfile.AudioSampleRate,
ffmpegProfile.NormalizeFramerate,
ffmpegProfile.DeinterlaceVideo);
private static ResolutionViewModel Project(Resolution resolution) =>
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
}
@@ -15,7 +15,7 @@ public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles,
GetAllFFmpegProfiles request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.FFmpegProfiles
.Include(p => p.Resolution)
.ToListAsync(cancellationToken)
@@ -0,0 +1,5 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.FFmpegProfiles;
public record GetSupportedHardwareAccelerationKinds : IRequest<List<HardwareAccelerationKind>>;
@@ -0,0 +1,79 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles;
public class
GetSupportedHardwareAccelerationKindsHandler : IRequestHandler<GetSupportedHardwareAccelerationKinds,
List<HardwareAccelerationKind>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
public GetSupportedHardwareAccelerationKindsHandler(
IDbContextFactory<TvContext> dbContextFactory,
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory)
{
_dbContextFactory = dbContextFactory;
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
}
public async Task<List<HardwareAccelerationKind>> Handle(
GetSupportedHardwareAccelerationKinds request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, string> validation = await Validate(dbContext);
return await validation.Match(
GetHardwareAccelerationKinds,
_ => Task.FromResult(new List<HardwareAccelerationKind> { HardwareAccelerationKind.None }));
}
private async Task<List<HardwareAccelerationKind>> GetHardwareAccelerationKinds(string ffmpegPath)
{
var result = new List<HardwareAccelerationKind> { HardwareAccelerationKind.None };
IFFmpegCapabilities ffmpegCapabilities = await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Nvenc))
{
result.Add(HardwareAccelerationKind.Nvenc);
}
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Qsv))
{
result.Add(HardwareAccelerationKind.Qsv);
}
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Vaapi))
{
result.Add(HardwareAccelerationKind.Vaapi);
}
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.VideoToolbox))
{
result.Add(HardwareAccelerationKind.VideoToolbox);
}
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Amf))
{
result.Add(HardwareAccelerationKind.Amf);
}
return result;
}
private static async Task<Validation<BaseError, string>> Validate(TvContext dbContext) =>
await FFmpegPathMustExist(dbContext);
private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) =>
dbContext.ConfigElements.GetValue<string>(ConfigElementKey.FFmpegPath)
.FilterT(File.Exists)
.Map(maybePath => maybePath.ToValidation<BaseError>("FFmpeg path does not exist on filesystem"));
}
@@ -17,7 +17,7 @@ public class DeleteFillerPresetHandler : IRequestHandler<DeleteFillerPreset, Eit
DeleteFillerPreset request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
return await validation.Apply(ps => DoDeletion(dbContext, ps));
}
@@ -28,7 +28,7 @@ public class DeleteFillerPresetHandler : IRequestHandler<DeleteFillerPreset, Eit
return dbContext.SaveChangesAsync().ToUnit();
}
private Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
private static Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
TvContext dbContext,
DeleteFillerPreset request) =>
dbContext.FillerPresets
@@ -17,10 +17,10 @@ public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Eit
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, ps => ApplyUpdateRequest(dbContext, ps, request));
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
}
private async Task<Unit> ApplyUpdateRequest(
private static async Task<Unit> ApplyUpdateRequest(
TvContext dbContext,
FillerPreset existing,
UpdateFillerPreset request)
@@ -1,5 +1,4 @@
using Dapper;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Filler.Mapper;
@@ -17,14 +16,12 @@ public class GetPagedFillerPresetsHandler : IRequestHandler<GetPagedFillerPreset
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
int count = await dbContext.Connection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM FillerPreset");
List<FillerPresetViewModel> page = await dbContext.FillerPresets.FromSqlRaw(
@"SELECT * FROM FillerPreset
ORDER BY Name
COLLATE NOCASE
LIMIT {0} OFFSET {1}",
request.PageSize,
request.PageNum * request.PageSize)
int count = await dbContext.FillerPresets.CountAsync(cancellationToken);
List<FillerPresetViewModel> page = await dbContext.FillerPresets
.AsNoTracking()
.OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation))
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
@@ -1,4 +1,5 @@
using ErsatzTV.Core;
using System.Globalization;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
@@ -15,7 +16,10 @@ public class UpdateHDHRTunerCountHandler : IRequestHandler<UpdateHDHRTunerCount,
UpdateHDHRTunerCount request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString()))
.MapT(
_ => _configElementRepository.Upsert(
ConfigElementKey.HDHRTunerCount,
request.TunerCount.ToString(CultureInfo.InvariantCulture)))
.Bind(v => v.ToEitherAsync());
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
@@ -0,0 +1,83 @@
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Jellyfin;
public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<SynchronizeJellyfinCollections>,
IRequestHandler<SynchronizeJellyfinCollections, Either<BaseError, Unit>>
{
public CallJellyfinCollectionScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
{
}
public async Task<Either<BaseError, Unit>>
Handle(SynchronizeJellyfinCollections request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request);
return await validation.Match(
scanner => PerformScan(scanner, request, cancellationToken),
error =>
{
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
{
return Task.FromResult<Either<BaseError, Unit>>(scanIsNotRequired);
}
return Task.FromResult<Either<BaseError, Unit>>(error.Join());
});
}
protected override async Task<DateTimeOffset> GetLastScan(TvContext dbContext, SynchronizeJellyfinCollections request)
{
DateTime minDateTime = await dbContext.JellyfinMediaSources
.SelectOneAsync(l => l.Id, l => l.Id == request.JellyfinMediaSourceId)
.Match(l => l.LastCollectionsScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
return new DateTimeOffset(minDateTime, TimeSpan.Zero);
}
protected override bool ScanIsRequired(
DateTimeOffset lastScan,
int libraryRefreshInterval,
SynchronizeJellyfinCollections request)
{
if (lastScan == SystemTime.MaxValueUtc)
{
return false;
}
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
}
private async Task<Either<BaseError, Unit>> PerformScan(
string scanner,
SynchronizeJellyfinCollections request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
{
"scan-jellyfin-collections", request.JellyfinMediaSourceId.ToString(CultureInfo.InvariantCulture)
};
if (request.ForceScan)
{
arguments.Add("--force");
}
return await base.PerformScan(scanner, arguments, cancellationToken).MapT(_ => Unit.Default);
}
}
@@ -1,4 +1,5 @@
using System.Threading.Channels;
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core;
using ErsatzTV.Core.Errors;
@@ -59,7 +60,7 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
{
var arguments = new List<string>
{
"scan-jellyfin", request.JellyfinLibraryId.ToString()
"scan-jellyfin", request.JellyfinLibraryId.ToString(CultureInfo.InvariantCulture)
};
if (request.ForceScan)
@@ -52,5 +52,5 @@ public class SaveJellyfinSecretsHandler : IRequestHandler<SaveJellyfinSecrets, E
return Unit.Default;
}
private record Parameters(JellyfinSecrets Secrets, JellyfinServerInformation ServerInformation);
private sealed record Parameters(JellyfinSecrets Secrets, JellyfinServerInformation ServerInformation);
}
@@ -98,7 +98,7 @@ public class
.ToValidation<BaseError>("Jellyfin media source requires an api key");
}
private record ConnectionParameters(
private sealed record ConnectionParameters(
JellyfinMediaSource JellyfinMediaSource,
JellyfinConnection ActiveConnection)
{
@@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Jellyfin;
public record SynchronizeJellyfinCollections(int JellyfinMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>,
IScannerBackgroundServiceRequest;
@@ -104,7 +104,7 @@ public class
return Unit.Default;
}
private record ConnectionParameters(
private sealed record ConnectionParameters(
JellyfinMediaSource JellyfinMediaSource,
JellyfinConnection ActiveConnection)
{
@@ -9,4 +9,4 @@ public record JellyfinLibraryViewModel(
LibraryMediaKind MediaKind,
bool ShouldSyncItems,
int MediaSourceId)
: LibraryViewModel("Jellyfin", Id, Name, MediaKind, MediaSourceId);
: LibraryViewModel("Jellyfin", Id, Name, MediaKind, MediaSourceId, string.Empty);
@@ -60,7 +60,7 @@ public class GetJellyfinConnectionParametersHandler : IRequestHandler<GetJellyfi
.ToValidation<BaseError>("Jellyfin media source requires an active connection");
}
private record ConnectionParameters(
private sealed record ConnectionParameters(
JellyfinMediaSource JellyfinMediaSource,
JellyfinConnection ActiveConnection);
}
@@ -94,7 +94,7 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
.SelectOneAsync(a => a.Id, a => a.Id == request.TargetLibraryId)
.Map(o => o.ToValidation<BaseError>("LocalLibrary does not exist"));
private async Task<string> GetPath(TvContext dbContext, MediaItem mediaItem) =>
private static async Task<string> GetPath(TvContext dbContext, MediaItem mediaItem) =>
mediaItem switch
{
Movie => await dbContext.Connection.QuerySingleAsync<string>(
@@ -115,5 +115,5 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
_ => null
};
private record Parameters(LibraryPath LibraryPath, LocalLibrary Library);
private sealed record Parameters(LibraryPath LibraryPath, LocalLibrary Library);
}
@@ -110,5 +110,5 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.ToUpperInvariant();
private record Parameters(LocalLibrary Existing, LocalLibrary Incoming);
private sealed record Parameters(LocalLibrary Existing, LocalLibrary Incoming);
}
@@ -2,4 +2,10 @@
namespace ErsatzTV.Application.Libraries;
public record LibraryViewModel(string LibraryKind, int Id, string Name, LibraryMediaKind MediaKind, int MediaSourceId);
public record LibraryViewModel(
string LibraryKind,
int Id,
string Name,
LibraryMediaKind MediaKind,
int MediaSourceId,
string MediaSourceName);
@@ -3,4 +3,4 @@
namespace ErsatzTV.Application.Libraries;
public record LocalLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, int MediaSourceId)
: LibraryViewModel("Local", Id, Name, MediaKind, MediaSourceId);
: LibraryViewModel("Local", Id, Name, MediaKind, MediaSourceId, string.Empty);
+13 -1
View File
@@ -10,7 +10,12 @@ internal static class Mapper
library switch
{
LocalLibrary l => ProjectToViewModel(l),
PlexLibrary p => new PlexLibraryViewModel(p.Id, p.Name, p.MediaKind, p.MediaSourceId),
PlexLibrary p => new PlexLibraryViewModel(
p.Id,
p.Name,
p.MediaKind,
p.MediaSourceId,
GetServerName(p.MediaSource)),
JellyfinLibrary j => new JellyfinLibraryViewModel(
j.Id,
j.Name,
@@ -26,4 +31,11 @@ internal static class Mapper
public static LocalLibraryPathViewModel ProjectToViewModel(LibraryPath libraryPath) =>
new(libraryPath.Id, libraryPath.LibraryId, libraryPath.Path);
private static string GetServerName(MediaSource ms) =>
ms switch
{
PlexMediaSource pms => pms.ServerName,
_ => string.Empty
};
}
@@ -2,5 +2,10 @@
namespace ErsatzTV.Application.Libraries;
public record PlexLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, int MediaSourceId)
: LibraryViewModel("Plex", Id, Name, MediaKind, MediaSourceId);
public record PlexLibraryViewModel(
int Id,
string Name,
LibraryMediaKind MediaKind,
int MediaSourceId,
string MediaSourceName)
: LibraryViewModel("Plex", Id, Name, MediaKind, MediaSourceId, MediaSourceName);
@@ -15,19 +15,51 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
GetExternalCollections request,
CancellationToken cancellationToken)
{
List<LibraryViewModel> result = new();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<int> mediaSourceIds = await dbContext.EmbyMediaSources
result.AddRange(await GetEmbyExternalCollections(dbContext, cancellationToken));
result.AddRange(await GetJellyfinExternalCollections(dbContext, cancellationToken));
result.AddRange(await GetPlexExternalCollections(dbContext, cancellationToken));
return result;
}
private static async Task<IEnumerable<LibraryViewModel>> GetEmbyExternalCollections(
TvContext dbContext,
CancellationToken cancellationToken)
{
List<int> embyMediaSourceIds = await dbContext.EmbyMediaSources
.Filter(ems => ems.Libraries.Any(l => ((EmbyLibrary)l).ShouldSyncItems))
.Map(ems => ems.Id)
.ToListAsync(cancellationToken);
return mediaSourceIds.Map(
id => new LibraryViewModel(
"Emby",
0,
"Collections",
0,
id))
.ToList();
return embyMediaSourceIds.Map(id => new LibraryViewModel("Emby", 0, "Collections", 0, id, string.Empty));
}
private static async Task<IEnumerable<LibraryViewModel>> GetJellyfinExternalCollections(
TvContext dbContext,
CancellationToken cancellationToken)
{
List<int> jellyfinMediaSourceIds = await dbContext.JellyfinMediaSources
.Filter(jms => jms.Libraries.Any(l => ((JellyfinLibrary)l).ShouldSyncItems))
.Map(jms => jms.Id)
.ToListAsync(cancellationToken);
return jellyfinMediaSourceIds.Map(
id => new LibraryViewModel("Jellyfin", 0, "Collections", 0, id, string.Empty));
}
private static async Task<IEnumerable<LibraryViewModel>> GetPlexExternalCollections(
TvContext dbContext,
CancellationToken cancellationToken)
{
List<int> plexMediaSourceIds = await dbContext.PlexMediaSources
.Filter(pms => pms.Libraries.Any(l => ((PlexLibrary)l).ShouldSyncItems))
.Map(pms => pms.Id)
.ToListAsync(cancellationToken);
return plexMediaSourceIds.Map(
id => new LibraryViewModel("Plex", 0, "Collections", 0, id, string.Empty));
}
}
+2 -3
View File
@@ -3,7 +3,7 @@ using Serilog.Events;
namespace ErsatzTV.Application.Logs;
internal partial class Mapper
internal sealed partial class Mapper
{
[GeneratedRegex(@"(.*)\[(DBG|INF|WRN|ERR|FTL)\](.*)")]
private static partial Regex LogEntryRegex();
@@ -11,12 +11,11 @@ internal partial class Mapper
internal static Option<LogEntryViewModel> ProjectToViewModel(string line)
{
Match match = LogEntryRegex().Match(line);
if (!match.Success)
if (!match.Success || !DateTimeOffset.TryParse(match.Groups[1].Value, out DateTimeOffset timestamp))
{
return None;
}
var timestamp = DateTimeOffset.Parse(match.Groups[1].Value);
LogEventLevel level = match.Groups[2].Value switch
{
"FTL" => LogEventLevel.Fatal,
@@ -29,21 +29,21 @@ public class EmptyTrashHandler : IRequestHandler<EmptyTrash, Either<BaseError, U
{
string[] types =
{
SearchIndex.MovieType,
SearchIndex.ShowType,
SearchIndex.SeasonType,
SearchIndex.EpisodeType,
SearchIndex.MusicVideoType,
SearchIndex.OtherVideoType,
SearchIndex.SongType,
SearchIndex.ArtistType
LuceneSearchIndex.MovieType,
LuceneSearchIndex.ShowType,
LuceneSearchIndex.SeasonType,
LuceneSearchIndex.EpisodeType,
LuceneSearchIndex.MusicVideoType,
LuceneSearchIndex.OtherVideoType,
LuceneSearchIndex.SongType,
LuceneSearchIndex.ArtistType
};
var ids = new List<int>();
foreach (string type in types)
{
SearchResult result = _searchIndex.Search(_client, $"type:{type} AND (state:FileNotFound)", 0, 0);
SearchResult result = await _searchIndex.Search(_client, $"type:{type} AND (state:FileNotFound)", 0, 0);
ids.AddRange(result.Items.Map(i => i.Id));
}
@@ -2,5 +2,5 @@ namespace ErsatzTV.Application.Maintenance;
public record ReleaseMemory(bool ForceAggressive) : IRequest, IBackgroundServiceRequest
{
public DateTimeOffset RequestTime = DateTimeOffset.Now;
public DateTimeOffset RequestTime { get; } = DateTimeOffset.Now;
}
@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record ArtistCardResultsViewModel(
int Count,
List<ArtistCardViewModel> Cards,
Option<SearchPageMap> PageMap);
SearchPageMap PageMap);
+14 -11
View File
@@ -1,4 +1,5 @@
using ErsatzTV.Core;
using System.Globalization;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Extensions;
@@ -15,7 +16,7 @@ internal static class Mapper
new(
showMetadata.ShowId,
showMetadata.Title,
showMetadata.Year?.ToString(),
showMetadata.Year?.ToString(CultureInfo.InvariantCulture),
showMetadata.SortTitle,
GetPoster(showMetadata, maybeJellyfin, maybeEmby),
showMetadata.Show.State);
@@ -33,7 +34,7 @@ internal static class Mapper
GetSeasonName(season.SeasonNumber),
season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby))
.IfNone(string.Empty),
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString(),
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString(CultureInfo.InvariantCulture),
season.State);
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
@@ -53,7 +54,9 @@ internal static class Mapper
GetSeasonName(seasonMetadata.Season.SeasonNumber),
$"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}",
GetPoster(seasonMetadata, maybeJellyfin, maybeEmby),
seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString(),
seasonMetadata.Season.SeasonNumber == 0
? "S"
: seasonMetadata.Season.SeasonNumber.ToString(CultureInfo.InvariantCulture),
seasonMetadata.Season.State);
}
@@ -94,7 +97,7 @@ internal static class Mapper
new(
movieMetadata.MovieId,
movieMetadata.Title,
movieMetadata.Year?.ToString(),
movieMetadata.Year?.ToString(CultureInfo.InvariantCulture),
movieMetadata.SortTitle,
GetPoster(movieMetadata, maybeJellyfin, maybeEmby),
movieMetadata.Movie.State);
@@ -181,12 +184,12 @@ internal static class Mapper
{
string artwork = actor.Artwork?.Path ?? string.Empty;
if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://"))
if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://", StringComparison.OrdinalIgnoreCase))
{
artwork = JellyfinUrl.RelativeProxyForArtwork(artwork)
.SetQueryParam("fillHeight", 440);
}
else if (maybeEmby.IsSome && artwork.StartsWith("emby://"))
else if (maybeEmby.IsSome && artwork.StartsWith("emby://", StringComparison.OrdinalIgnoreCase))
{
artwork = EmbyUrl.RelativeProxyForArtwork(artwork)
.SetQueryParam("maxHeight", 440);
@@ -229,12 +232,12 @@ internal static class Mapper
string poster = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster))
.Match(a => a.Path, string.Empty);
if (maybeJellyfin.IsSome && poster.StartsWith("jellyfin://"))
if (maybeJellyfin.IsSome && poster.StartsWith("jellyfin://", StringComparison.OrdinalIgnoreCase))
{
poster = JellyfinUrl.RelativeProxyForArtwork(poster)
.SetQueryParam("fillHeight", 440);
}
else if (maybeEmby.IsSome && poster.StartsWith("emby://"))
else if (maybeEmby.IsSome && poster.StartsWith("emby://", StringComparison.OrdinalIgnoreCase))
{
poster = EmbyUrl.RelativeProxyForArtwork(poster)
.SetQueryParam("maxHeight", 440);
@@ -251,12 +254,12 @@ internal static class Mapper
string thumb = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Thumbnail))
.Match(a => a.Path, string.Empty);
if (maybeJellyfin.IsSome && thumb.StartsWith("jellyfin://"))
if (maybeJellyfin.IsSome && thumb.StartsWith("jellyfin://", StringComparison.OrdinalIgnoreCase))
{
thumb = JellyfinUrl.RelativeProxyForArtwork(thumb)
.SetQueryParam("fillHeight", 220);
}
else if (maybeEmby.IsSome && thumb.StartsWith("emby://"))
else if (maybeEmby.IsSome && thumb.StartsWith("emby://", StringComparison.OrdinalIgnoreCase))
{
thumb = EmbyUrl.RelativeProxyForArtwork(thumb)
.SetQueryParam("maxHeight", 220);
@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.MediaCards;
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards, Option<SearchPageMap> PageMap);
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards, SearchPageMap PageMap);
@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record MusicVideoCardResultsViewModel(
int Count,
List<MusicVideoCardViewModel> Cards,
Option<SearchPageMap> PageMap);
SearchPageMap PageMap);
@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record OtherVideoCardResultsViewModel(
int Count,
List<OtherVideoCardViewModel> Cards,
Option<SearchPageMap> PageMap);
SearchPageMap PageMap);
@@ -49,6 +49,6 @@ public class GetMusicVideoCardsHandler : IRequestHandler<GetMusicVideoCards, Mus
results.Add(ProjectToViewModel(musicVideoMetadata, localPath));
}
return new MusicVideoCardResultsViewModel(count, results, None);
return new MusicVideoCardResultsViewModel(count, results, null);
}
}
@@ -4,7 +4,6 @@ using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Search;
using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards;
@@ -59,6 +58,6 @@ public class
results.Add(ProjectToViewModel(episodeMetadata, maybeJellyfin, maybeEmby, false, localPath));
}
return new TelevisionEpisodeCardResultsViewModel(count, results, Option<SearchPageMap>.None);
return new TelevisionEpisodeCardResultsViewModel(count, results, null);
}
}
@@ -35,6 +35,6 @@ public class
.GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize)
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList());
return new TelevisionSeasonCardResultsViewModel(count, results, None);
return new TelevisionSeasonCardResultsViewModel(count, results, null);
}
}
@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record SongCardResultsViewModel(
int Count,
List<SongCardViewModel> Cards,
Option<SearchPageMap> PageMap);
SearchPageMap PageMap);
@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record TelevisionEpisodeCardResultsViewModel(
int Count,
List<TelevisionEpisodeCardViewModel> Cards,
Option<SearchPageMap> PageMap);
SearchPageMap PageMap);
@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record TelevisionSeasonCardResultsViewModel(
int Count,
List<TelevisionSeasonCardViewModel> Cards,
Option<SearchPageMap> PageMap);
SearchPageMap PageMap);
@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record TelevisionShowCardResultsViewModel(
int Count,
List<TelevisionShowCardViewModel> Cards,
Option<SearchPageMap> PageMap);
SearchPageMap PageMap);
@@ -73,5 +73,5 @@ public class AddArtistToCollectionHandler :
.SelectOneAsync(a => a.Id, a => a.Id == request.ArtistId)
.Map(o => o.ToValidation<BaseError>("Artist does not exist"));
private record Parameters(Collection Collection, Artist Artist);
private sealed record Parameters(Collection Collection, Artist Artist);
}
@@ -75,5 +75,5 @@ public class AddEpisodeToCollectionHandler :
.SelectOneAsync(e => e.Id, e => e.Id == request.EpisodeId)
.Map(o => o.ToValidation<BaseError>("Episode does not exist"));
private record Parameters(Collection Collection, Episode Episode);
private sealed record Parameters(Collection Collection, Episode Episode);
}
@@ -73,5 +73,5 @@ public class AddMovieToCollectionHandler :
.SelectOneAsync(m => m.Id, e => e.Id == request.MovieId)
.Map(o => o.ToValidation<BaseError>("Movie does not exist"));
private record Parameters(Collection Collection, Movie Movie);
private sealed record Parameters(Collection Collection, Movie Movie);
}
@@ -75,5 +75,5 @@ public class AddMusicVideoToCollectionHandler :
.SelectOneAsync(m => m.Id, e => e.Id == request.MusicVideoId)
.Map(o => o.ToValidation<BaseError>("MusicVideo does not exist"));
private record Parameters(Collection Collection, MusicVideo MusicVideo);
private sealed record Parameters(Collection Collection, MusicVideo MusicVideo);
}
@@ -75,5 +75,5 @@ public class AddOtherVideoToCollectionHandler :
.SelectOneAsync(m => m.Id, e => e.Id == request.OtherVideoId)
.Map(o => o.ToValidation<BaseError>("OtherVideo does not exist"));
private record Parameters(Collection Collection, OtherVideo OtherVideo);
private sealed record Parameters(Collection Collection, OtherVideo OtherVideo);
}
@@ -73,5 +73,5 @@ public class AddSeasonToCollectionHandler :
.SelectOneAsync(m => m.Id, e => e.Id == request.SeasonId)
.Map(o => o.ToValidation<BaseError>("Season does not exist"));
private record Parameters(Collection Collection, Season Season);
private sealed record Parameters(Collection Collection, Season Season);
}
@@ -73,5 +73,5 @@ public class AddShowToCollectionHandler :
.SelectOneAsync(m => m.Id, e => e.Id == request.ShowId)
.Map(o => o.ToValidation<BaseError>("Show does not exist"));
private record Parameters(Collection Collection, Show Show);
private sealed record Parameters(Collection Collection, Show Show);
}
@@ -73,5 +73,5 @@ public class AddSongToCollectionHandler :
.SelectOneAsync(m => m.Id, e => e.Id == request.SongId)
.Map(o => o.ToValidation<BaseError>("Song does not exist"));
private record Parameters(Collection Collection, Song Song);
private sealed record Parameters(Collection Collection, Song Song);
}
@@ -72,5 +72,5 @@ public class AddTraktListHandler : TraktCommandBase, IRequestHandler<AddTraktLis
// match list items (and update in search index)
}
private record Parameters(string User, string List);
private sealed record Parameters(string User, string List);
}
@@ -43,7 +43,7 @@ public abstract class TraktCommandBase
.SelectOneAsync(c => c.Id, c => c.Id == traktListId)
.Map(o => o.ToValidation<BaseError>($"TraktList {traktListId} does not exist."));
protected async Task<Either<BaseError, TraktList>> SaveList(TvContext dbContext, TraktList list)
protected static async Task<Either<BaseError, TraktList>> SaveList(TvContext dbContext, TraktList list)
{
Option<TraktList> maybeExisting = await dbContext.TraktLists
.Include(l => l.Items)
@@ -199,7 +199,7 @@ public abstract class TraktCommandBase
return result;
}
private async Task<Option<int>> IdentifyMovie(TvContext dbContext, TraktListItem item)
private static async Task<Option<int>> IdentifyMovie(TvContext dbContext, TraktListItem item)
{
var guids = item.Guids.Map(g => g.Guid).ToList();
@@ -234,7 +234,7 @@ public abstract class TraktCommandBase
return None;
}
private async Task<Option<int>> IdentifyShow(TvContext dbContext, TraktListItem item)
private static async Task<Option<int>> IdentifyShow(TvContext dbContext, TraktListItem item)
{
var guids = item.Guids.Map(g => g.Guid).ToList();
@@ -269,7 +269,7 @@ public abstract class TraktCommandBase
return None;
}
private async Task<Option<int>> IdentifySeason(TvContext dbContext, TraktListItem item)
private static async Task<Option<int>> IdentifySeason(TvContext dbContext, TraktListItem item)
{
var guids = item.Guids.Map(g => g.Guid).ToList();
@@ -305,7 +305,7 @@ public abstract class TraktCommandBase
return None;
}
private async Task<Option<int>> IdentifyEpisode(TvContext dbContext, TraktListItem item)
private static async Task<Option<int>> IdentifyEpisode(TvContext dbContext, TraktListItem item)
{
var guids = item.Guids.Map(g => g.Guid).ToList();
@@ -1,5 +1,4 @@
using Dapper;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper;
@@ -17,14 +16,12 @@ public class GetPagedCollectionsHandler : IRequestHandler<GetPagedCollections, P
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
int count = await dbContext.Connection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM Collection");
List<MediaCollectionViewModel> page = await dbContext.Collections.FromSqlRaw(
@"SELECT * FROM Collection
ORDER BY Name
COLLATE NOCASE
LIMIT {0} OFFSET {1}",
request.PageSize,
request.PageNum * request.PageSize)
int count = await dbContext.Collections.CountAsync(cancellationToken);
List<MediaCollectionViewModel> page = await dbContext.Collections
.AsNoTracking()
.OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation))
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
@@ -1,5 +1,4 @@
using Dapper;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper;
@@ -17,14 +16,12 @@ public class GetPagedMultiCollectionsHandler : IRequestHandler<GetPagedMultiColl
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
int count = await dbContext.Connection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM MultiCollection");
List<MultiCollectionViewModel> page = await dbContext.MultiCollections.FromSqlRaw(
@"SELECT * FROM MultiCollection
ORDER BY Name
COLLATE NOCASE
LIMIT {0} OFFSET {1}",
request.PageSize,
request.PageNum * request.PageSize)
int count = await dbContext.MultiCollections.CountAsync(cancellationToken);
List<MultiCollectionViewModel> page = await dbContext.MultiCollections
.AsNoTracking()
.OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation))
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.Include(mc => mc.MultiCollectionItems)
.ThenInclude(i => i.Collection)
.ToListAsync(cancellationToken)
@@ -1,5 +1,4 @@
using Dapper;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper;
@@ -17,14 +16,12 @@ public class GetPagedSmartCollectionsHandler : IRequestHandler<GetPagedSmartColl
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
int count = await dbContext.Connection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM SmartCollection");
List<SmartCollectionViewModel> page = await dbContext.SmartCollections.FromSqlRaw(
@"SELECT * FROM SmartCollection
ORDER BY Name
COLLATE NOCASE
LIMIT {0} OFFSET {1}",
request.PageSize,
request.PageNum * request.PageSize)
int count = await dbContext.SmartCollections.CountAsync(cancellationToken);
List<SmartCollectionViewModel> page = await dbContext.SmartCollections
.AsNoTracking()
.OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation))
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
@@ -1,5 +1,4 @@
using Dapper;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper;
@@ -17,14 +16,12 @@ public class GetPagedTraktListsHandler : IRequestHandler<GetPagedTraktLists, Pag
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
int count = await dbContext.Connection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM TraktList");
List<TraktListViewModel> page = await dbContext.TraktLists.FromSqlRaw(
@"SELECT * FROM TraktList
ORDER BY Name
COLLATE NOCASE
LIMIT {0} OFFSET {1}",
request.PageSize,
request.PageNum * request.PageSize)
int count = await dbContext.TraktLists.CountAsync(cancellationToken);
List<TraktListViewModel> page = await dbContext.TraktLists
.AsNoTracking()
.OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation))
.Skip(request.PageNum * request.PageSize)
.Take(request.PageSize)
.Include(l => l.Items)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.MediaItems;
// ReSharper disable once InconsistentNaming
public record LanguageCodeViewModel(string ThreeLetterISOLanguageName, string EnglishName);
@@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaItems;
public record MediaItemInfoStream(
int Index,
int? Index,
MediaStreamKind Kind,
string Title,
string Codec,
@@ -1,5 +1,3 @@
using System.Globalization;
namespace ErsatzTV.Application.MediaItems;
namespace ErsatzTV.Application.MediaItems;
public record GetAllLanguageCodes : IRequest<List<CultureInfo>>;
public record GetAllLanguageCodes : IRequest<List<LanguageCodeViewModel>>;
@@ -3,13 +3,18 @@ using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.MediaItems;
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<CultureInfo>>
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<LanguageCodeViewModel>>
{
private readonly IMediaItemRepository _mediaItemRepository;
public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) =>
_mediaItemRepository = mediaItemRepository;
public async Task<List<CultureInfo>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken) =>
await _mediaItemRepository.GetAllLanguageCodeCultures();
public async Task<List<LanguageCodeViewModel>> Handle(
GetAllLanguageCodes request,
CancellationToken cancellationToken)
{
List<CultureInfo> cultures = await _mediaItemRepository.GetAllLanguageCodeCultures();
return cultures.Map(c => new LanguageCodeViewModel(c.ThreeLetterISOLanguageName, c.EnglishName)).ToList();
}
}
@@ -28,10 +28,14 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
.ThenInclude(lp => lp.Library)
.ThenInclude(l => l.MediaSource)
// TODO: support all media types here
.Include(i => (i as Movie).MovieMetadata)
.ThenInclude(mv => mv.Subtitles)
.Include(i => (i as Movie).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => (i as Episode).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => (i as Episode).EpisodeMetadata)
.ThenInclude(mv => mv.Subtitles)
.SelectOneAsync(i => i.Id, i => i.Id == request.Id)
.MapT(Project);
@@ -55,6 +59,18 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
_ => null
};
List<Subtitle> subtitles = mediaItem switch
{
Movie m => m.MovieMetadata.Map(mm => mm.Subtitles).Flatten().ToList(),
Episode e => e.EpisodeMetadata.Map(mm => mm.Subtitles).Flatten().ToList(),
_ => new List<Subtitle>()
};
var allStreams = version.Streams.OrderBy(s => s.Index).Map(Project).ToList();
// include external subtitles from local libraries
allStreams.AddRange(subtitles.Filter(s => s.SubtitleKind is SubtitleKind.Sidecar).Map(ProjectToStream));
return new MediaItemInfo(
mediaItem.Id,
mediaItem.GetType().Name,
@@ -69,7 +85,7 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
version.VideoScanKind,
version.Width,
version.Height,
version.Streams.OrderBy(s => s.Index).Map(Project).ToList());
allStreams);
}
private static MediaItemInfoStream Project(MediaStream mediaStream) =>
@@ -92,4 +108,25 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
mediaStream.BitsPerRawSample > 0 ? mediaStream.BitsPerRawSample : null,
mediaStream.FileName,
mediaStream.MimeType);
private static MediaItemInfoStream ProjectToStream(Subtitle subtitle) =>
new(
null,
MediaStreamKind.ExternalSubtitle,
subtitle.Title,
subtitle.Codec,
null,
subtitle.Language,
null,
subtitle.Default ? true : null,
subtitle.Forced ? true : null,
null,
null,
null,
null,
null,
null,
null,
string.IsNullOrWhiteSpace(subtitle.Path) ? null : Path.GetFileName(subtitle.Path),
null);
}
@@ -1,4 +1,5 @@
using System.Threading.Channels;
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@@ -55,7 +56,7 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
{
var arguments = new List<string>
{
"scan-local", request.LibraryId.ToString()
"scan-local", request.LibraryId.ToString(CultureInfo.InvariantCulture)
};
if (request.ForceScan)
+6 -6
View File
@@ -19,7 +19,7 @@ internal static class Mapper
MovieMetadata metadata = Optional(movie.MovieMetadata).Flatten().Head();
return new MovieViewModel(
metadata.Title,
metadata.Year?.ToString(),
metadata.Year?.ToString(CultureInfo.InvariantCulture),
metadata.Plot,
metadata.Genres.Map(g => g.Name).ToList(),
metadata.Tags.Map(t => t.Name).ToList(),
@@ -41,17 +41,17 @@ internal static class Mapper
};
}
private static List<CultureInfo> LanguagesForMovie(List<string> languageCodes)
private static List<string> LanguagesForMovie(List<string> languageCodes)
{
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languageCodes
.Distinct()
.Map(
lang => allCultures.Filter(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence()
.Flatten()
.Map(ci => ci.EnglishName)
.Distinct()
.ToList();
}
@@ -64,7 +64,7 @@ internal static class Mapper
string artwork = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty);
if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://"))
if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://", StringComparison.OrdinalIgnoreCase))
{
Url url = JellyfinUrl.RelativeProxyForArtwork(artwork);
if (artworkKind is ArtworkKind.Poster or ArtworkKind.Thumbnail)
@@ -74,7 +74,7 @@ internal static class Mapper
artwork = url;
}
else if (maybeEmby.IsSome && artwork.StartsWith("emby://"))
else if (maybeEmby.IsSome && artwork.StartsWith("emby://", StringComparison.OrdinalIgnoreCase))
{
Url url = EmbyUrl.RelativeProxyForArtwork(artwork);
if (artworkKind is ArtworkKind.Poster or ArtworkKind.Thumbnail)

Some files were not shown because too many files have changed in this diff Show More