Compare commits
109
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
663a62431b | ||
|
|
1d4acc284d | ||
|
|
0440f7643b | ||
|
|
0f4219f731 | ||
|
|
cbe5d47611 | ||
|
|
afa52ccc89 | ||
|
|
7d1163c68f | ||
|
|
883492bd33 | ||
|
|
a4eac4feea | ||
|
|
dab58f5840 | ||
|
|
176f136c23 | ||
|
|
816d77e15b | ||
|
|
7c4d47a211 | ||
|
|
d9d2cfa8be | ||
|
|
8036e46966 | ||
|
|
594ce437fb | ||
|
|
004c43f895 | ||
|
|
257384ea9b | ||
|
|
637f3a0c8b | ||
|
|
7346808059 | ||
|
|
4210d97ee2 | ||
|
|
6a8ecd2532 | ||
|
|
9b834f7cbe | ||
|
|
7b73677bad | ||
|
|
85b2a46353 | ||
|
|
6f40f2cbd6 | ||
|
|
b62ee4dee9 | ||
|
|
a6e7f192cc | ||
|
|
59a1a4a8dc | ||
|
|
85a9afb51c | ||
|
|
246b4d7591 | ||
|
|
ae2c6350e1 | ||
|
|
ce228604e8 | ||
|
|
3656e932d3 | ||
|
|
73887706ed | ||
|
|
abc103308b | ||
|
|
3773bbec19 | ||
|
|
e223d6a43f | ||
|
|
8369111e31 | ||
|
|
35ba2bab2c | ||
|
|
094ed71ad0 | ||
|
|
89e24b2b78 | ||
|
|
848795af32 | ||
|
|
56f94f489a | ||
|
|
475dc7660b | ||
|
|
db3dfbd446 | ||
|
|
b4c9cdbbfa | ||
|
|
7f84933c0b | ||
|
|
1e35e9a5b0 | ||
|
|
7edf6f5d13 | ||
|
|
919325033d | ||
|
|
2cb5252320 | ||
|
|
015232fad6 | ||
|
|
af51b790b6 | ||
|
|
9195ef7878 | ||
|
|
dfc4c7a284 | ||
|
|
a6b15f68c9 | ||
|
|
0edfb71f8d | ||
|
|
21b90a1b6c | ||
|
|
1582f5dd15 | ||
|
|
fd3b72525d | ||
|
|
55d1871d94 | ||
|
|
a90eb2d4de | ||
|
|
ed3f1b1dad | ||
|
|
8e08ff059f | ||
|
|
fb8c3a0453 | ||
|
|
e45fb67769 | ||
|
|
3a40d6ce77 | ||
|
|
ac048b72ae | ||
|
|
852728c816 | ||
|
|
096f2d42e8 | ||
|
|
1b29e252ff | ||
|
|
a4dc9bfb31 | ||
|
|
184c21a91b | ||
|
|
6ea3191cf8 | ||
|
|
d487bbca08 | ||
|
|
05034b47e2 | ||
|
|
b0c85b6478 | ||
|
|
f1356563da | ||
|
|
c0aad028a8 | ||
|
|
dae06ec0ef | ||
|
|
72f452fd36 | ||
|
|
aaf832c0b6 | ||
|
|
08a18daf23 | ||
|
|
90c1c61a09 | ||
|
|
053db71d44 | ||
|
|
11f90f5d44 | ||
|
|
bda4117655 | ||
|
|
3240703840 | ||
|
|
53a7570ba3 | ||
|
|
0e789fd6d8 | ||
|
|
0136de700c | ||
|
|
2ea0e64ac1 | ||
|
|
5993f23ec5 | ||
|
|
417f35a834 | ||
|
|
a74547997d | ||
|
|
a2f74dd284 | ||
|
|
373daf9ce6 | ||
|
|
68693cffa0 | ||
|
|
6d147de2f3 | ||
|
|
f4a63a1a1a | ||
|
|
bc9d17ca25 | ||
|
|
42e13cbbaf | ||
|
|
6cc61f3212 | ||
|
|
4cf44616a8 | ||
|
|
33aaadae68 | ||
|
|
fe3f8e391e | ||
|
|
1a68dd040a | ||
|
|
67761c1a14 |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"jetbrains.resharper.globaltools": {
|
"jetbrains.resharper.globaltools": {
|
||||||
"version": "2021.1.3",
|
"version": "2021.2.2",
|
||||||
"commands": [
|
"commands": [
|
||||||
"jb"
|
"jb"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: 5.0.x
|
dotnet-version: 6.0.x
|
||||||
|
|
||||||
- name: Clean
|
- name: Clean
|
||||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout master
|
- name: Checkout master
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Deploy docs
|
- name: Deploy docs
|
||||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ jobs:
|
|||||||
kind: windows
|
kind: windows
|
||||||
target: win-x64
|
target: win-x64
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
kind: maxOS
|
kind: macOS
|
||||||
target: osx-x64
|
target: osx-x64
|
||||||
|
- os: macos-latest
|
||||||
|
kind: macOS
|
||||||
|
target: osx-arm64
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
@@ -28,7 +31,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: 5.0.x
|
dotnet-version: 6.0.x
|
||||||
|
|
||||||
- name: Clean
|
- name: Clean
|
||||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
@@ -44,14 +47,11 @@ jobs:
|
|||||||
release_name="ErsatzTV-$tag-${{ matrix.target }}"
|
release_name="ErsatzTV-$tag-${{ matrix.target }}"
|
||||||
|
|
||||||
# Build everything
|
# Build everything
|
||||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:PublishSingleFile=true --self-contained true
|
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:EnableCompressionInSingleFile=true /property:DebugType=Embedded /property:PublishSingleFile=true --self-contained true
|
||||||
|
|
||||||
# Pack files
|
# Pack files
|
||||||
if [ "${{ matrix.target }}" == "win-x64" ]; then
|
if [ "${{ matrix.target }}" == "win-x64" ]; then
|
||||||
7z a -tzip "${release_name}.zip" "./${release_name}/*"
|
7z a -tzip "${release_name}.zip" "./${release_name}/*"
|
||||||
elif [ "${{ matrix.target }}" == "linux-arm" ]; then
|
|
||||||
cp lib/linux-arm/* "$release_name/"
|
|
||||||
tar czvf "${release_name}.tar.gz" "$release_name"
|
|
||||||
else
|
else
|
||||||
tar czvf "${release_name}.tar.gz" "$release_name"
|
tar czvf "${release_name}.tar.gz" "$release_name"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+213
-1
@@ -4,8 +4,205 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.3.7-alpha] - 2022-01-17
|
||||||
|
### Fixed
|
||||||
|
- Fix local folder scanners to properly detect removed/re-added folders with unchanged contents
|
||||||
|
- Fix double-click startup on mac
|
||||||
|
- Fix trakt list sync when show does not contain a year
|
||||||
|
- Properly unlock libraries when a scan is unable to be performed because ffmpeg or ffprobe have not been found
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add trash system for local libraries to maintain collection and schedule integrity through media share outages
|
||||||
|
- When items are missing from disk, they will be flagged and present in the `Media` > `Trash` page
|
||||||
|
- The trash page can be used to permanently remove missing items from the database
|
||||||
|
- When items reappear at the expected location on disk, they will be unflagged and removed from the trash
|
||||||
|
- Add basic Mac hardware acceleration using VideoToolbox
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Local libraries only: when items are missing from disk, they will be added to the trash and no longer removed from collections, etc.
|
||||||
|
- Show song thumbnail in song list
|
||||||
|
|
||||||
|
## [0.3.6-alpha] - 2022-01-10
|
||||||
|
### Fixed
|
||||||
|
- Properly index `minutes` field when adding new items during scan (vs when rebuilding index)
|
||||||
|
- Fix some nvenc edge cases where only padding is needed for normalization
|
||||||
|
- Properly overwrite environment variables for ffmpeg processes (`LIBVA_DRIVER_NAME`, `FFREPORT`)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add music video `artist` to search index
|
||||||
|
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Remove `HLS Hybrid` streaming mode; all channels have been reconfigured to use the superior `HLS Segmenter` streaming mode
|
||||||
|
- Update `MPEG-TS` streaming mode to internally use the HLS segmenter
|
||||||
|
- This improves compatibility with many clients and also improves performance at program boundaries
|
||||||
|
- Renamed existing `MPEG-TS` mode as `MPEG-TS (Legacy)`
|
||||||
|
- This mode will be removed in a future release
|
||||||
|
|
||||||
|
## [0.3.5-alpha] - 2022-01-05
|
||||||
|
### Fixed
|
||||||
|
- Fix bundled ffmpeg version in base docker image (NOT nvidia or vaapi) which prevented playback since v0.3.0-alpha
|
||||||
|
- Use software decoding for mpeg4 content when VAAPI acceleration is enabled
|
||||||
|
- Fix hardware acceleration health check to recognize QSV on non-Windows platforms
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Treat `setsar` as a hardware filter, avoiding unneeded `hwdownload` and `hwupload` steps when padding isn't required
|
||||||
|
|
||||||
|
## [0.3.4-alpha] - 2021-12-21
|
||||||
|
### Fixed
|
||||||
|
- Fix other video and song scanners to include videos contained directly in top-level folders that are added to a library
|
||||||
|
- Allow saving ffmpeg troubleshooting reports on Windows
|
||||||
|
|
||||||
|
## [0.3.3-alpha] - 2021-12-12
|
||||||
|
### Fixed
|
||||||
|
- Fix bug with saving multiple blurhash versions for cover art; all cover art will be automatically rescanned
|
||||||
|
- Fix song detail margin when no cover art exists and no watermark exists
|
||||||
|
- Fix synchronizing virtual shows and seasons from Jellyfin
|
||||||
|
- Properly sort channels in M3U
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use blurhash of ErsatzTV colors instead of solid colors for default song backgrounds
|
||||||
|
- Use select control instead of autocomplete control in many places
|
||||||
|
- The autocomplete control is not intuitive to use and has focus bugs
|
||||||
|
|
||||||
|
## [0.3.2-alpha] - 2021-12-03
|
||||||
|
### Fixed
|
||||||
|
- Fix artwork upload on Windows
|
||||||
|
- Fix unicode song metadata on Windows
|
||||||
|
- Fix unicode console output on Windows
|
||||||
|
- Fix TV Show NFO metadata processing when `year` is missing
|
||||||
|
- Fix song detail outline to help legibility on white backgrounds
|
||||||
|
- Optimize song artwork scanning to prevent re-processing album artwork for each song
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use custom log database backend which should be more portable (i.e. work in osx-arm64)
|
||||||
|
- Use cover art blurhashes for song backgrounds instead of solid colors or box blur
|
||||||
|
|
||||||
|
## [0.3.1-alpha] - 2021-11-30
|
||||||
|
### Fixed
|
||||||
|
- Fix song page links in UI
|
||||||
|
- Show song artist in playout detail
|
||||||
|
- Include song artist and cover art in channel guide (xmltv)
|
||||||
|
- Use subtitles to display errors, which fixes many edge cases of unescaped characters
|
||||||
|
- Properly split song genre tags
|
||||||
|
- Properly display all songs that have an identical album and title
|
||||||
|
- Fix channel logo and watermark uploads
|
||||||
|
- Fix regression introduced with `v0.2.4-alpha` that caused some filler edge cases to crash the playout builder
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add song genres to search index
|
||||||
|
- Use embedded song cover art when sidecar cover art is unavailable
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Randomly place song cover art on left or right side of screen
|
||||||
|
- Randomly use a solid color from the cover art instead of blurred cover art for song background
|
||||||
|
- Randomly select song detail layout (large title/small artist or small artist/title/album)
|
||||||
|
|
||||||
|
## [0.3.0-alpha] - 2021-11-25
|
||||||
|
### Fixed
|
||||||
|
- Properly fix database incompatibility introduced with `v0.2.4-alpha` and partially fixed with `v0.2.5-alpha`
|
||||||
|
- The proper fix requires rebuilding all playouts, which will happen on startup after upgrading
|
||||||
|
- Fix local library locking/progress display when adding paths
|
||||||
|
- Fix grouping duration items in EPG when custom title is configured
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add *experimental* `Songs` local libraries
|
||||||
|
- Like `Other Videos`, `Songs` require no metadata or particular folder layout, and will have tags added for each containing folder
|
||||||
|
- For Example, a song at `rock/band/1990 - Album/01 whatever.flac` will have the tags `rock`, `band` and `1990 - Album`, and the title `01 whatever`
|
||||||
|
- Songs will also have basic metadata read from embedded tags (album, artist, title)
|
||||||
|
- Video will be automatically generated for songs using metadata and cover art or watermarks if available
|
||||||
|
- Add support for `.webm` video files
|
||||||
|
|
||||||
|
## [0.2.5-alpha] - 2021-11-21
|
||||||
|
### Fixed
|
||||||
|
- Include other video title in channel guide (xmltv)
|
||||||
|
- Fix bug introduced with 0.2.4-alpha that caused some playouts to build from year 0
|
||||||
|
- Use less memory matching Trakt list items
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Build osx-arm64 packages on release
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- No longer warn about local Plex guids; they aren't used for Trakt matching and can be ignored
|
||||||
|
|
||||||
|
## [0.2.4-alpha] - 2021-11-13
|
||||||
|
### Changed
|
||||||
|
- Upgrade to dotnet 6
|
||||||
|
- Use `scale_cuda` instead of `scale_npp` for NVIDIA scaling in all cases
|
||||||
|
|
||||||
|
## [0.2.3-alpha] - 2021-11-03
|
||||||
|
### Fixed
|
||||||
|
- Fix bug with audio filter in cultures where `.` is a group/thousands separator
|
||||||
|
- Fix bug where flood playout mode would only schedule one item
|
||||||
|
- This would happen if the flood was followed by another flood with a fixed start time
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Support empty `.etvignore` file to instruct local movie scanner to ignore the containing folder
|
||||||
|
|
||||||
|
## [0.2.2-alpha] - 2021-10-30
|
||||||
|
### Fixed
|
||||||
|
- Fix EPG entries for Duration schedule items that play multiple items
|
||||||
|
- Fix EPG entries for Multiple schedule items that play more than one item
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add fallback filler settings to Channel and global FFmpeg Settings
|
||||||
|
- When streaming is attempted during an unscheduled gap, the resulting video will be determined using the following priority:
|
||||||
|
- Channel fallback filler
|
||||||
|
- Global fallback filler
|
||||||
|
- Generated `Channel Is Offline` error message video
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Allow per-episode folders for local show libraries
|
||||||
|
- e.g. `Show Name\Season #\Episode #\Show Name - s#e#.mkv`
|
||||||
|
|
||||||
|
## [0.2.1-alpha] - 2021-10-24
|
||||||
|
### Fixed
|
||||||
|
- Fix saving dynamic start time on schedule items
|
||||||
|
|
||||||
|
## [0.2.0-alpha] - 2021-10-23
|
||||||
|
### Fixed
|
||||||
|
- Fix generated streams with mpeg2video
|
||||||
|
- Fix incorrect row count in playout detail table
|
||||||
|
- Fix deleting movies that have been removed from Jellyfin and Emby
|
||||||
|
- Fix bug that caused large unscheduled gaps in playouts
|
||||||
|
- This was caused by schedule items with a fixed start of midnight
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add new filler system
|
||||||
|
- `Pre-Roll Filler` plays before each media item
|
||||||
|
- `Mid-Roll Filler` plays between media item chapters
|
||||||
|
- `Post-Roll Filler` plays after each media item
|
||||||
|
- `Tail Filler` plays after all media items, until the next media item
|
||||||
|
- `Fallback Filler` loops instead of default offline image to fill any remaining gaps
|
||||||
|
- Store chapter details with media statistics; this is needed to support mid-roll filler
|
||||||
|
- This requires re-ingesting statistics for all media items the first time this version is launched
|
||||||
|
- Add switch to show/hide filler in playout detail table
|
||||||
|
- Add `minutes` field to search index
|
||||||
|
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Change some debug log messages to info so they show by default again
|
||||||
|
- Remove tail collection options from `Duration` playout mode
|
||||||
|
- Show localized start time in schedule items tables
|
||||||
|
|
||||||
|
## [0.1.5-alpha] - 2021-10-18
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fix double scheduling; this could happen if the app was shutdown during a playout build
|
- Fix double scheduling; this could happen if the app was shutdown during a playout build
|
||||||
|
- Fix updating Jellyfin and Emby TV seasons
|
||||||
|
- Fix updating Jellyfin and Emby artwork
|
||||||
|
- Fix Plex, Jellyfin, Emby worker crash attempting to sync library that no longer exists
|
||||||
|
- Fix bug with `Duration` mode scheduling when media items are too long to fit in the requested duration
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Include music video thumbnails in channel guide (xmltv)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Automatically find working Plex address on startup
|
||||||
|
- Automatically select schedule item in schedules that contain only one item
|
||||||
|
- Change default log level from `Debug` to `Information`
|
||||||
|
- The `Debug` log level can be enabled in the `appsettings.json` file for non-docker installs
|
||||||
|
- The `Debug` log level can be enabled by setting the environment variable `Serilog:MinimumLevel=Debug` for docker installs
|
||||||
|
|
||||||
## [0.1.4-alpha] - 2021-10-14
|
## [0.1.4-alpha] - 2021-10-14
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -714,7 +911,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- Initial release to facilitate testing outside of Docker.
|
- Initial release to facilitate testing outside of Docker.
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.4-alpha...HEAD
|
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.7-alpha...HEAD
|
||||||
|
[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.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.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.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.1-alpha...v0.1.2-alpha
|
||||||
|
|||||||
@@ -10,5 +10,7 @@ namespace ErsatzTV.Application.Channels
|
|||||||
string Logo,
|
string Logo,
|
||||||
string PreferredLanguageCode,
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId);
|
int? WatermarkId,
|
||||||
|
int? FallbackFillerId,
|
||||||
|
int PlayoutCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
string Logo,
|
string Logo,
|
||||||
string PreferredLanguageCode,
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId) : IRequest<Either<BaseError, CreateChannelResult>>;
|
int? WatermarkId,
|
||||||
|
int? FallbackFillerId) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -42,9 +43,10 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
(ValidateName(request), await ValidateNumber(dbContext, request),
|
(ValidateName(request), await ValidateNumber(dbContext, request),
|
||||||
await FFmpegProfileMustExist(dbContext, request),
|
await FFmpegProfileMustExist(dbContext, request),
|
||||||
ValidatePreferredLanguage(request),
|
ValidatePreferredLanguage(request),
|
||||||
await WatermarkMustExist(dbContext, request))
|
await WatermarkMustExist(dbContext, request),
|
||||||
|
await FillerPresetMustExist(dbContext, request))
|
||||||
.Apply(
|
.Apply(
|
||||||
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId) =>
|
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) =>
|
||||||
{
|
{
|
||||||
var artwork = new List<Artwork>();
|
var artwork = new List<Artwork>();
|
||||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
if (!string.IsNullOrWhiteSpace(request.Logo))
|
||||||
@@ -74,6 +76,11 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
channel.WatermarkId = id;
|
channel.WatermarkId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (int id in fillerPresetId)
|
||||||
|
{
|
||||||
|
channel.FallbackFillerId = id;
|
||||||
|
}
|
||||||
|
|
||||||
return channel;
|
return channel;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,5 +138,25 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
.MapT(_ => Optional(createChannel.WatermarkId))
|
.MapT(_ => Optional(createChannel.WatermarkId))
|
||||||
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
|
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, Option<int>>> FillerPresetMustExist(
|
||||||
|
TvContext dbContext,
|
||||||
|
CreateChannel createChannel)
|
||||||
|
{
|
||||||
|
if (createChannel.FallbackFillerId is null)
|
||||||
|
{
|
||||||
|
return Option<int>.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await dbContext.FillerPresets
|
||||||
|
.Filter(fp => fp.FillerKind == FillerKind.Fallback)
|
||||||
|
.CountAsync(w => w.Id == createChannel.FallbackFillerId)
|
||||||
|
.Map(Optional)
|
||||||
|
.Filter(c => c > 0)
|
||||||
|
.MapT(_ => Optional(createChannel.FallbackFillerId))
|
||||||
|
.Map(
|
||||||
|
o => o.ToValidation<BaseError>(
|
||||||
|
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
string Logo,
|
string Logo,
|
||||||
string PreferredLanguageCode,
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId) : IRequest<Either<BaseError, ChannelViewModel>>;
|
int? WatermarkId,
|
||||||
|
int? FallbackFillerId) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
|
|
||||||
c.StreamingMode = update.StreamingMode;
|
c.StreamingMode = update.StreamingMode;
|
||||||
c.WatermarkId = update.WatermarkId;
|
c.WatermarkId = update.WatermarkId;
|
||||||
|
c.FallbackFillerId = update.FallbackFillerId;
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
return ProjectToViewModel(c);
|
return ProjectToViewModel(c);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ namespace ErsatzTV.Application.Channels
|
|||||||
GetLogo(channel),
|
GetLogo(channel),
|
||||||
channel.PreferredLanguageCode,
|
channel.PreferredLanguageCode,
|
||||||
channel.StreamingMode,
|
channel.StreamingMode,
|
||||||
channel.WatermarkId);
|
channel.WatermarkId,
|
||||||
|
channel.FallbackFillerId,
|
||||||
|
channel.Playouts?.Count ?? 0);
|
||||||
|
|
||||||
private static string GetLogo(Channel channel) =>
|
private static string GetLogo(Channel channel) =>
|
||||||
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace ErsatzTV.Application.Configuration.Commands
|
|||||||
|
|
||||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
|
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
|
||||||
Optional(request.LibraryRefreshInterval)
|
Optional(request.LibraryRefreshInterval)
|
||||||
.Filter(lri => lri > 0)
|
.Where(lri => lri > 0)
|
||||||
.Map(_ => Unit.Default)
|
.Map(_ => Unit.Default)
|
||||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||||
.AsTask();
|
.AsTask();
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ namespace ErsatzTV.Application.Configuration.Commands
|
|||||||
|
|
||||||
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
|
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
|
||||||
Optional(request.DaysToBuild)
|
Optional(request.DaysToBuild)
|
||||||
.Filter(days => days > 0)
|
.Where(days => days > 0)
|
||||||
.Map(_ => Unit.Default)
|
.Map(_ => Unit.Default)
|
||||||
.ToValidation<BaseError>("Days to build must be greater than zero")
|
.ToValidation<BaseError>("Days to build must be greater than zero")
|
||||||
.AsTask();
|
.AsTask();
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ namespace ErsatzTV.Application.Emby.Commands
|
|||||||
{
|
{
|
||||||
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ namespace ErsatzTV.Application.Emby.Commands
|
|||||||
{
|
{
|
||||||
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<NoWarn>VSTHRD200</NoWarn>
|
<NoWarn>VSTHRD200</NoWarn>
|
||||||
<DebugType>embedded</DebugType>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.63">
|
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
@@ -16,16 +14,13 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
{
|
{
|
||||||
private readonly IConfigElementRepository _configElementRepository;
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
private readonly ILocalFileSystem _localFileSystem;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
private readonly IRuntimeInfo _runtimeInfo;
|
|
||||||
|
|
||||||
public UpdateFFmpegSettingsHandler(
|
public UpdateFFmpegSettingsHandler(
|
||||||
IConfigElementRepository configElementRepository,
|
IConfigElementRepository configElementRepository,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem)
|
||||||
IRuntimeInfo runtimeInfo)
|
|
||||||
{
|
{
|
||||||
_configElementRepository = configElementRepository;
|
_configElementRepository = configElementRepository;
|
||||||
_localFileSystem = localFileSystem;
|
_localFileSystem = localFileSystem;
|
||||||
_runtimeInfo = runtimeInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle(
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
@@ -36,8 +31,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
.Bind(v => v.ToEitherAsync());
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
|
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
|
||||||
(await FFmpegMustExist(request), await FFprobeMustExist(request), ReportsAreNotSupportedOnWindows(request))
|
(await FFmpegMustExist(request), await FFprobeMustExist(request))
|
||||||
.Apply((_, _, _) => Unit.Default);
|
.Apply((_, _) => Unit.Default);
|
||||||
|
|
||||||
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
|
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
|
||||||
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
|
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
|
||||||
@@ -45,16 +40,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
|
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
|
||||||
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
|
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
|
||||||
|
|
||||||
private Validation<BaseError, Unit> ReportsAreNotSupportedOnWindows(UpdateFFmpegSettings request)
|
|
||||||
{
|
|
||||||
if (request.Settings.SaveReports && _runtimeInfo.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
return BaseError.New("FFmpeg reports are not supported on Windows");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Unit.Default;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
|
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
|
||||||
{
|
{
|
||||||
if (!_localFileSystem.FileExists(path))
|
if (!_localFileSystem.FileExists(path))
|
||||||
@@ -115,6 +100,17 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId);
|
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.Settings.GlobalFallbackFillerId is not null)
|
||||||
|
{
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.FFmpegGlobalFallbackFillerId,
|
||||||
|
request.Settings.GlobalFallbackFillerId.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId);
|
||||||
|
}
|
||||||
|
|
||||||
await _configElementRepository.Upsert(
|
await _configElementRepository.Upsert(
|
||||||
ConfigElementKey.FFmpegSegmenterTimeout,
|
ConfigElementKey.FFmpegSegmenterTimeout,
|
||||||
request.Settings.HlsSegmenterIdleTimeout);
|
request.Settings.HlsSegmenterIdleTimeout);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
public string PreferredLanguageCode { get; set; }
|
public string PreferredLanguageCode { get; set; }
|
||||||
public bool SaveReports { get; set; }
|
public bool SaveReports { get; set; }
|
||||||
public int? GlobalWatermarkId { get; set; }
|
public int? GlobalWatermarkId { get; set; }
|
||||||
|
public int? GlobalFallbackFillerId { get; set; }
|
||||||
public int HlsSegmenterIdleTimeout { get; set; }
|
public int HlsSegmenterIdleTimeout { get; set; }
|
||||||
public int WorkAheadSegmenterLimit { get; set; }
|
public int WorkAheadSegmenterLimit { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
|||||||
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||||
Option<int> watermark =
|
Option<int> watermark =
|
||||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||||
|
Option<int> fallbackFiller =
|
||||||
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId);
|
||||||
Option<int> hlsSegmenterIdleTimeout =
|
Option<int> hlsSegmenterIdleTimeout =
|
||||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
|
||||||
Option<int> workAheadSegmenterLimit =
|
Option<int> workAheadSegmenterLimit =
|
||||||
@@ -49,6 +51,11 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
|||||||
result.GlobalWatermarkId = watermarkId;
|
result.GlobalWatermarkId = watermarkId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (int fallbackFillerId in fallbackFiller)
|
||||||
|
{
|
||||||
|
result.GlobalFallbackFillerId = fallbackFillerId;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public record CreateFillerPreset(
|
||||||
|
string Name,
|
||||||
|
FillerKind FillerKind,
|
||||||
|
FillerMode FillerMode,
|
||||||
|
TimeSpan? Duration,
|
||||||
|
int? Count,
|
||||||
|
int? PadToNearestMinute,
|
||||||
|
ProgramScheduleItemCollectionType CollectionType,
|
||||||
|
int? CollectionId,
|
||||||
|
int? MediaItemId,
|
||||||
|
int? MultiCollectionId,
|
||||||
|
int? SmartCollectionId
|
||||||
|
) : IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public CreateFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(CreateFillerPreset request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
var fillerPreset = new FillerPreset
|
||||||
|
{
|
||||||
|
Name = request.Name,
|
||||||
|
FillerKind = request.FillerKind,
|
||||||
|
FillerMode = request.FillerMode,
|
||||||
|
Duration = request.Duration,
|
||||||
|
Count = request.Count,
|
||||||
|
PadToNearestMinute = request.PadToNearestMinute,
|
||||||
|
CollectionType = request.CollectionType,
|
||||||
|
CollectionId = request.CollectionId,
|
||||||
|
MediaItemId = request.MediaItemId,
|
||||||
|
MultiCollectionId = request.MultiCollectionId,
|
||||||
|
SmartCollectionId = request.SmartCollectionId
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BaseError.New(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public record DeleteFillerPreset(int FillerPresetId) : IRequest<Either<BaseError, LanguageExt.Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public class DeleteFillerPresetHandler : IRequestHandler<DeleteFillerPreset, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public DeleteFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
DeleteFillerPreset request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
|
||||||
|
return await validation.Apply(ps => DoDeletion(dbContext, ps));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<Unit> DoDeletion(TvContext dbContext, FillerPreset fillerPreset)
|
||||||
|
{
|
||||||
|
dbContext.FillerPresets.Remove(fillerPreset);
|
||||||
|
return dbContext.SaveChangesAsync().ToUnit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
|
||||||
|
TvContext dbContext,
|
||||||
|
DeleteFillerPreset request) =>
|
||||||
|
dbContext.FillerPresets
|
||||||
|
.SelectOneAsync(fp => fp.Id, ps => ps.Id == request.FillerPresetId)
|
||||||
|
.Map(o => o.ToValidation<BaseError>($"FillerPreset {request.FillerPresetId} does not exist."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public record UpdateFillerPreset(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
FillerKind FillerKind,
|
||||||
|
FillerMode FillerMode,
|
||||||
|
TimeSpan? Duration,
|
||||||
|
int? Count,
|
||||||
|
int? PadToNearestMinute,
|
||||||
|
ProgramScheduleItemCollectionType CollectionType,
|
||||||
|
int? CollectionId,
|
||||||
|
int? MediaItemId,
|
||||||
|
int? MultiCollectionId,
|
||||||
|
int? SmartCollectionId
|
||||||
|
) : IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public UpdateFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(UpdateFillerPreset request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
|
||||||
|
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdateRequest(
|
||||||
|
TvContext dbContext,
|
||||||
|
FillerPreset existing,
|
||||||
|
UpdateFillerPreset request)
|
||||||
|
{
|
||||||
|
existing.Name = request.Name;
|
||||||
|
existing.FillerKind = request.FillerKind;
|
||||||
|
existing.FillerMode = request.FillerMode;
|
||||||
|
existing.Duration = request.Duration;
|
||||||
|
existing.Count = request.Count;
|
||||||
|
existing.PadToNearestMinute = request.PadToNearestMinute;
|
||||||
|
existing.CollectionType = request.CollectionType;
|
||||||
|
existing.CollectionId = request.CollectionId;
|
||||||
|
existing.MediaItemId = request.MediaItemId;
|
||||||
|
existing.MultiCollectionId = request.MultiCollectionId;
|
||||||
|
existing.SmartCollectionId = request.SmartCollectionId;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
|
||||||
|
TvContext dbContext,
|
||||||
|
UpdateFillerPreset request) =>
|
||||||
|
dbContext.FillerPresets
|
||||||
|
.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id)
|
||||||
|
.Map(o => o.ToValidation<BaseError>("FillerPreset does not exist"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler
|
||||||
|
{
|
||||||
|
public record FillerPresetViewModel(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
FillerKind FillerKind,
|
||||||
|
FillerMode FillerMode,
|
||||||
|
TimeSpan? Duration,
|
||||||
|
int? Count,
|
||||||
|
int? PadToNearestMinute,
|
||||||
|
ProgramScheduleItemCollectionType CollectionType,
|
||||||
|
int? CollectionId,
|
||||||
|
int? MediaItemId,
|
||||||
|
int? MultiCollectionId,
|
||||||
|
int? SmartCollectionId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler
|
||||||
|
{
|
||||||
|
internal static class Mapper
|
||||||
|
{
|
||||||
|
internal static FillerPresetViewModel ProjectToViewModel(FillerPreset fillerPreset) =>
|
||||||
|
new(
|
||||||
|
fillerPreset.Id,
|
||||||
|
fillerPreset.Name,
|
||||||
|
fillerPreset.FillerKind,
|
||||||
|
fillerPreset.FillerMode,
|
||||||
|
fillerPreset.Duration,
|
||||||
|
fillerPreset.Count,
|
||||||
|
fillerPreset.PadToNearestMinute,
|
||||||
|
fillerPreset.CollectionType,
|
||||||
|
fillerPreset.CollectionId,
|
||||||
|
fillerPreset.MediaItemId,
|
||||||
|
fillerPreset.MultiCollectionId,
|
||||||
|
fillerPreset.SmartCollectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler
|
||||||
|
{
|
||||||
|
public record PagedFillerPresetsViewModel(int TotalCount, List<FillerPresetViewModel> Page);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public record GetAllFillerPresets : IRequest<List<FillerPresetViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using LanguageExt;
|
||||||
|
using static ErsatzTV.Application.Filler.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public class GetAllFillerPresetsHandler : IRequestHandler<GetAllFillerPresets, List<FillerPresetViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public GetAllFillerPresetsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
|
||||||
|
public async Task<List<FillerPresetViewModel>> Handle(
|
||||||
|
GetAllFillerPresets request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
return await dbContext.FillerPresets.ToListAsync(cancellationToken)
|
||||||
|
.Map(presets => presets.Map(ProjectToViewModel).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public record GetFillerPresetById(int Id) : IRequest<Option<FillerPresetViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using static ErsatzTV.Application.Filler.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public class GetFillerPresetByIdHandler : IRequestHandler<GetFillerPresetById, Option<FillerPresetViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public GetFillerPresetByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
|
||||||
|
public async Task<Option<FillerPresetViewModel>> Handle(
|
||||||
|
GetFillerPresetById request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
return await dbContext.FillerPresets
|
||||||
|
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
||||||
|
.MapT(ProjectToViewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public record GetPagedFillerPresets(int PageNum, int PageSize) : IRequest<PagedFillerPresetsViewModel>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using LanguageExt;
|
||||||
|
using static ErsatzTV.Application.Filler.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public class GetPagedFillerPresetsHandler : IRequestHandler<GetPagedFillerPresets, PagedFillerPresetsViewModel>
|
||||||
|
{
|
||||||
|
private readonly IDbConnection _dbConnection;
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public GetPagedFillerPresetsHandler(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_dbConnection = dbConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedFillerPresetsViewModel> Handle(
|
||||||
|
GetPagedFillerPresets request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int count = await _dbConnection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM FillerPreset");
|
||||||
|
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
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)
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||||
|
|
||||||
|
return new PagedFillerPresetsViewModel(count, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ namespace ErsatzTV.Application.HDHR.Commands
|
|||||||
|
|
||||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
||||||
Optional(request.TunerCount)
|
Optional(request.TunerCount)
|
||||||
.Filter(tc => tc > 0)
|
.Where(tc => tc > 0)
|
||||||
.Map(_ => Unit.Default)
|
.Map(_ => Unit.Default)
|
||||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||||
.AsTask();
|
.AsTask();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using ErsatzTV.Core;
|
using System.IO;
|
||||||
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -6,5 +7,5 @@ using MediatR;
|
|||||||
namespace ErsatzTV.Application.Images.Commands
|
namespace ErsatzTV.Application.Images.Commands
|
||||||
{
|
{
|
||||||
// ReSharper disable once SuggestBaseTypeForParameter
|
// ReSharper disable once SuggestBaseTypeForParameter
|
||||||
public record SaveArtworkToDisk(byte[] Buffer, ArtworkKind ArtworkKind) : IRequest<Either<BaseError, string>>;
|
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind) : IRequest<Either<BaseError, string>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ namespace ErsatzTV.Application.Images.Commands
|
|||||||
public SaveArtworkToDiskHandler(IImageCache imageCache) => _imageCache = imageCache;
|
public SaveArtworkToDiskHandler(IImageCache imageCache) => _imageCache = imageCache;
|
||||||
|
|
||||||
public Task<Either<BaseError, string>> Handle(SaveArtworkToDisk request, CancellationToken cancellationToken) =>
|
public Task<Either<BaseError, string>> Handle(SaveArtworkToDisk request, CancellationToken cancellationToken) =>
|
||||||
_imageCache.SaveArtworkToCache(request.Buffer, request.ArtworkKind);
|
_imageCache.SaveArtworkToCache(request.Stream, request.ArtworkKind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
|||||||
{
|
{
|
||||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
|||||||
{
|
{
|
||||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
|||||||
{
|
{
|
||||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Linq;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -37,7 +36,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
CreateLocalLibrary request,
|
CreateLocalLibrary request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, LocalLibrary> validation = await Validate(dbContext, request);
|
Validation<BaseError, LocalLibrary> validation = await Validate(dbContext, request);
|
||||||
return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary));
|
return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
.Map(list => list.Map(c => c.Path).ToList());
|
.Map(list => list.Map(c => c.Path).ToList());
|
||||||
|
|
||||||
return Optional(request.Path)
|
return Optional(request.Path)
|
||||||
.Filter(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
|
.Where(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
|
||||||
.ToValidation<BaseError>("Path must not belong to another library path");
|
.ToValidation<BaseError>("Path must not belong to another library path");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
.Map(list => list.SelectMany(ll => ll.Paths).Map(lp => lp.Path).ToList());
|
.Map(list => list.SelectMany(ll => ll.Paths).Map(lp => lp.Path).ToList());
|
||||||
|
|
||||||
return Optional(localLibrary.Paths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder.Path))))
|
return Optional(localLibrary.Paths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder.Path))))
|
||||||
.Filter(length => length == 0)
|
.Where(length => length == 0)
|
||||||
.Map(_ => localLibrary)
|
.Map(_ => localLibrary)
|
||||||
.ToValidation<BaseError>("Path must not belong to another library path");
|
.ToValidation<BaseError>("Path must not belong to another library path");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
UpdateLocalLibrary request,
|
UpdateLocalLibrary request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||||
return await validation.Apply(parameters => UpdateLocalLibrary(dbContext, parameters));
|
return await validation.Apply(parameters => UpdateLocalLibrary(dbContext, parameters));
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,6 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
(LocalLibrary existing, LocalLibrary incoming) = parameters;
|
(LocalLibrary existing, LocalLibrary incoming) = parameters;
|
||||||
existing.Name = incoming.Name;
|
existing.Name = incoming.Name;
|
||||||
|
|
||||||
// toAdd
|
|
||||||
var toAdd = incoming.Paths
|
var toAdd = incoming.Paths
|
||||||
.Filter(p => existing.Paths.All(ep => NormalizePath(ep.Path) != NormalizePath(p.Path)))
|
.Filter(p => existing.Paths.All(ep => NormalizePath(ep.Path) != NormalizePath(p.Path)))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -77,7 +76,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
_searchIndex.Commit();
|
_searchIndex.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toAdd.Count > 0 || toRemove.Count > 0 && _entityLocker.LockLibrary(existing.Id))
|
if ((toAdd.Count > 0 || toRemove.Count > 0) && _entityLocker.LockLibrary(existing.Id))
|
||||||
{
|
{
|
||||||
await _workerChannel.WriteAsync(new ForceScanLocalLibrary(existing.Id));
|
await _workerChannel.WriteAsync(new ForceScanLocalLibrary(existing.Id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Maintenance.Commands;
|
||||||
|
|
||||||
|
public record DeleteItemsFromDatabase(List<int> MediaItemIds) : IRequest<Either<BaseError, Unit>>;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Maintenance.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
DeleteItemsFromDatabaseHandler : MediatR.IRequestHandler<DeleteItemsFromDatabase, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
|
||||||
|
public DeleteItemsFromDatabaseHandler(
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
|
ISearchIndex searchIndex)
|
||||||
|
{
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
DeleteItemsFromDatabase request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Either<BaseError, Unit> deleteResult = await _mediaItemRepository.DeleteItems(request.MediaItemIds);
|
||||||
|
if (deleteResult.IsRight)
|
||||||
|
{
|
||||||
|
await _searchIndex.RemoveItems(request.MediaItemIds);
|
||||||
|
_searchIndex.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb) :
|
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb, MediaItemState State) :
|
||||||
MediaCardViewModel(Id, Name, Role, Name, Thumb);
|
MediaCardViewModel(Id, Name, Role, Name, Thumb, State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record ArtistCardViewModel
|
public record ArtistCardViewModel
|
||||||
(int ArtistId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(
|
||||||
|
int ArtistId,
|
||||||
|
string Title,
|
||||||
|
string Subtitle,
|
||||||
|
string SortTitle,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
ArtistId,
|
ArtistId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster);
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
List<TelevisionEpisodeCardViewModel> EpisodeCards,
|
List<TelevisionEpisodeCardViewModel> EpisodeCards,
|
||||||
List<ArtistCardViewModel> ArtistCards,
|
List<ArtistCardViewModel> ArtistCards,
|
||||||
List<MusicVideoCardViewModel> MusicVideoCards,
|
List<MusicVideoCardViewModel> MusicVideoCards,
|
||||||
List<OtherVideoCardViewModel> OtherVideoCards)
|
List<OtherVideoCardViewModel> OtherVideoCards,
|
||||||
|
List<SongCardViewModel> SongCards)
|
||||||
{
|
{
|
||||||
public bool UseCustomPlaybackOrder { get; set; }
|
public bool UseCustomPlaybackOrder { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Emby;
|
using ErsatzTV.Core.Emby;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
using ErsatzTV.Core.Jellyfin;
|
using ErsatzTV.Core.Jellyfin;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using static LanguageExt.Prelude;
|
using static LanguageExt.Prelude;
|
||||||
@@ -19,7 +20,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
showMetadata.Title,
|
showMetadata.Title,
|
||||||
showMetadata.Year?.ToString(),
|
showMetadata.Year?.ToString(),
|
||||||
showMetadata.SortTitle,
|
showMetadata.SortTitle,
|
||||||
GetPoster(showMetadata, maybeJellyfin, maybeEmby));
|
GetPoster(showMetadata, maybeJellyfin, maybeEmby),
|
||||||
|
showMetadata.Show.State);
|
||||||
|
|
||||||
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
||||||
Season season,
|
Season season,
|
||||||
@@ -34,7 +36,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
GetSeasonName(season.SeasonNumber),
|
GetSeasonName(season.SeasonNumber),
|
||||||
season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby))
|
season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby))
|
||||||
.IfNone(string.Empty),
|
.IfNone(string.Empty),
|
||||||
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString());
|
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString(),
|
||||||
|
season.State);
|
||||||
|
|
||||||
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
||||||
SeasonMetadata seasonMetadata,
|
SeasonMetadata seasonMetadata,
|
||||||
@@ -53,7 +56,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
GetSeasonName(seasonMetadata.Season.SeasonNumber),
|
GetSeasonName(seasonMetadata.Season.SeasonNumber),
|
||||||
$"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}",
|
$"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}",
|
||||||
GetPoster(seasonMetadata, maybeJellyfin, maybeEmby),
|
GetPoster(seasonMetadata, maybeJellyfin, maybeEmby),
|
||||||
seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString());
|
seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString(),
|
||||||
|
seasonMetadata.Season.State);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
|
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
|
||||||
@@ -80,7 +84,9 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby)
|
? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby)
|
||||||
: GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby),
|
: GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby),
|
||||||
episodeMetadata.Directors.Map(d => d.Name).ToList(),
|
episodeMetadata.Directors.Map(d => d.Name).ToList(),
|
||||||
episodeMetadata.Writers.Map(w => w.Name).ToList());
|
episodeMetadata.Writers.Map(w => w.Name).ToList(),
|
||||||
|
episodeMetadata.Episode.State,
|
||||||
|
episodeMetadata.Episode.GetHeadVersion().MediaFiles.Head().Path);
|
||||||
|
|
||||||
internal static MovieCardViewModel ProjectToViewModel(
|
internal static MovieCardViewModel ProjectToViewModel(
|
||||||
MovieMetadata movieMetadata,
|
MovieMetadata movieMetadata,
|
||||||
@@ -91,7 +97,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
movieMetadata.Title,
|
movieMetadata.Title,
|
||||||
movieMetadata.Year?.ToString(),
|
movieMetadata.Year?.ToString(),
|
||||||
movieMetadata.SortTitle,
|
movieMetadata.SortTitle,
|
||||||
GetPoster(movieMetadata, maybeJellyfin, maybeEmby));
|
GetPoster(movieMetadata, maybeJellyfin, maybeEmby),
|
||||||
|
movieMetadata.Movie.State);
|
||||||
|
|
||||||
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
||||||
new(
|
new(
|
||||||
@@ -101,14 +108,29 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
musicVideoMetadata.SortTitle,
|
musicVideoMetadata.SortTitle,
|
||||||
musicVideoMetadata.Plot,
|
musicVideoMetadata.Plot,
|
||||||
musicVideoMetadata.Album,
|
musicVideoMetadata.Album,
|
||||||
GetThumbnail(musicVideoMetadata, None, None));
|
GetThumbnail(musicVideoMetadata, None, None),
|
||||||
|
musicVideoMetadata.MusicVideo.State,
|
||||||
|
musicVideoMetadata.MusicVideo.GetHeadVersion().MediaFiles.Head().Path);
|
||||||
|
|
||||||
internal static OtherVideoCardViewModel ProjectToViewModel(OtherVideoMetadata otherVideoMetadata) =>
|
internal static OtherVideoCardViewModel ProjectToViewModel(OtherVideoMetadata otherVideoMetadata) =>
|
||||||
new(
|
new(
|
||||||
otherVideoMetadata.OtherVideoId,
|
otherVideoMetadata.OtherVideoId,
|
||||||
otherVideoMetadata.Title,
|
otherVideoMetadata.Title,
|
||||||
otherVideoMetadata.OriginalTitle,
|
otherVideoMetadata.OriginalTitle,
|
||||||
otherVideoMetadata.SortTitle);
|
otherVideoMetadata.SortTitle,
|
||||||
|
otherVideoMetadata.OtherVideo.State);
|
||||||
|
|
||||||
|
internal static SongCardViewModel ProjectToViewModel(SongMetadata songMetadata)
|
||||||
|
{
|
||||||
|
string album = string.IsNullOrWhiteSpace(songMetadata.Album) ? "" : $" - {songMetadata.Album}";
|
||||||
|
return new SongCardViewModel(
|
||||||
|
songMetadata.SongId,
|
||||||
|
songMetadata.Title,
|
||||||
|
songMetadata.Artist + album,
|
||||||
|
songMetadata.SortTitle,
|
||||||
|
GetThumbnail(songMetadata, None, None),
|
||||||
|
songMetadata.Song.State);
|
||||||
|
}
|
||||||
|
|
||||||
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
||||||
new(
|
new(
|
||||||
@@ -116,7 +138,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
artistMetadata.Title,
|
artistMetadata.Title,
|
||||||
artistMetadata.Disambiguation,
|
artistMetadata.Disambiguation,
|
||||||
artistMetadata.SortTitle,
|
artistMetadata.SortTitle,
|
||||||
GetThumbnail(artistMetadata, None, None));
|
GetThumbnail(artistMetadata, None, None),
|
||||||
|
artistMetadata.Artist.State);
|
||||||
|
|
||||||
internal static CollectionCardResultsViewModel
|
internal static CollectionCardResultsViewModel
|
||||||
ProjectToViewModel(
|
ProjectToViewModel(
|
||||||
@@ -141,7 +164,9 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
||||||
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
collection.MediaItems.OfType<OtherVideo>().Map(mv => ProjectToViewModel(mv.OtherVideoMetadata.Head()))
|
collection.MediaItems.OfType<OtherVideo>().Map(ov => ProjectToViewModel(ov.OtherVideoMetadata.Head()))
|
||||||
|
.ToList(),
|
||||||
|
collection.MediaItems.OfType<Song>().Map(s => ProjectToViewModel(s.SongMetadata.Head()))
|
||||||
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||||
|
|
||||||
internal static ActorCardViewModel ProjectToViewModel(
|
internal static ActorCardViewModel ProjectToViewModel(
|
||||||
@@ -162,7 +187,7 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
.SetQueryParam("maxHeight", 440);
|
.SetQueryParam("maxHeight", 440);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork);
|
return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork, MediaItemState.Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MediaCardViewModel(int MediaItemId, string Title, string Subtitle, string SortTitle, string Poster);
|
public record MediaCardViewModel(
|
||||||
|
int MediaItemId,
|
||||||
|
string Title,
|
||||||
|
string Subtitle,
|
||||||
|
string SortTitle,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MovieCardViewModel
|
public record MovieCardViewModel
|
||||||
(int MovieId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(
|
||||||
|
int MovieId,
|
||||||
|
string Title,
|
||||||
|
string Subtitle,
|
||||||
|
string SortTitle,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
MovieId,
|
MovieId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster)
|
Poster,
|
||||||
|
State)
|
||||||
{
|
{
|
||||||
public int CustomIndex { get; set; }
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MusicVideoCardViewModel
|
public record MusicVideoCardViewModel
|
||||||
(
|
(
|
||||||
@@ -8,12 +10,15 @@
|
|||||||
string SortTitle,
|
string SortTitle,
|
||||||
string Plot,
|
string Plot,
|
||||||
string Album,
|
string Album,
|
||||||
string Poster) : MediaCardViewModel(
|
string Poster,
|
||||||
|
MediaItemState State,
|
||||||
|
string Path) : MediaCardViewModel(
|
||||||
MusicVideoId,
|
MusicVideoId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster)
|
Poster,
|
||||||
|
State)
|
||||||
{
|
{
|
||||||
public int CustomIndex { get; set; }
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record OtherVideoCardViewModel
|
public record OtherVideoCardViewModel
|
||||||
(
|
(
|
||||||
int OtherVideoId,
|
int OtherVideoId,
|
||||||
string Title,
|
string Title,
|
||||||
string Subtitle,
|
string Subtitle,
|
||||||
string SortTitle) : MediaCardViewModel(
|
string SortTitle,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
OtherVideoId,
|
OtherVideoId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
null)
|
null,
|
||||||
|
State)
|
||||||
{
|
{
|
||||||
public int CustomIndex { get; set; }
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
|||||||
GetCollectionCards request,
|
GetCollectionCards request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||||
.Map(list => list.HeadOrNone());
|
.Map(list => list.HeadOrNone());
|
||||||
@@ -83,6 +83,9 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
|||||||
.Include(c => c.MediaItems)
|
.Include(c => c.MediaItems)
|
||||||
.ThenInclude(i => (i as OtherVideo).OtherVideoMetadata)
|
.ThenInclude(i => (i as OtherVideo).OtherVideoMetadata)
|
||||||
.ThenInclude(ovm => ovm.Artwork)
|
.ThenInclude(ovm => ovm.Artwork)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as Song).SongMetadata)
|
||||||
|
.ThenInclude(ovm => ovm.Artwork)
|
||||||
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
||||||
.Map(c => c.ToEither(BaseError.New("Unable to load collection")))
|
.Map(c => c.ToEither(BaseError.New("Unable to load collection")))
|
||||||
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
|
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
|
{
|
||||||
|
public record SongCardResultsViewModel(
|
||||||
|
int Count,
|
||||||
|
List<SongCardViewModel> Cards,
|
||||||
|
Option<SearchPageMap> PageMap);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
|
{
|
||||||
|
public record SongCardViewModel
|
||||||
|
(
|
||||||
|
int SongId,
|
||||||
|
string Title,
|
||||||
|
string Subtitle,
|
||||||
|
string SortTitle,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
|
SongId,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
SortTitle,
|
||||||
|
Poster,
|
||||||
|
State)
|
||||||
|
{
|
||||||
|
public int CustomIndex { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
@@ -17,10 +18,13 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
string Plot,
|
string Plot,
|
||||||
string Poster,
|
string Poster,
|
||||||
List<string> Directors,
|
List<string> Directors,
|
||||||
List<string> Writers) : MediaCardViewModel(
|
List<string> Writers,
|
||||||
|
MediaItemState State,
|
||||||
|
string Path) : MediaCardViewModel(
|
||||||
EpisodeId,
|
EpisodeId,
|
||||||
Title,
|
Title,
|
||||||
$"Episode {Episode}",
|
$"Episode {Episode}",
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster);
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record TelevisionSeasonCardViewModel
|
public record TelevisionSeasonCardViewModel
|
||||||
(
|
(
|
||||||
@@ -9,10 +11,12 @@
|
|||||||
string Subtitle,
|
string Subtitle,
|
||||||
string SortTitle,
|
string SortTitle,
|
||||||
string Poster,
|
string Poster,
|
||||||
string Placeholder) : MediaCardViewModel(
|
string Placeholder,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
TelevisionSeasonId,
|
TelevisionSeasonId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster);
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record TelevisionShowCardViewModel
|
public record TelevisionShowCardViewModel
|
||||||
(int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(
|
||||||
|
int TelevisionShowId,
|
||||||
|
string Title,
|
||||||
|
string Subtitle,
|
||||||
|
string SortTitle,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
TelevisionShowId,
|
TelevisionShowId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster);
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
List<int> EpisodeIds,
|
List<int> EpisodeIds,
|
||||||
List<int> ArtistIds,
|
List<int> ArtistIds,
|
||||||
List<int> MusicVideoIds,
|
List<int> MusicVideoIds,
|
||||||
List<int> OtherVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
List<int> OtherVideoIds,
|
||||||
|
List<int> SongIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.Append(request.ArtistIds)
|
.Append(request.ArtistIds)
|
||||||
.Append(request.MusicVideoIds)
|
.Append(request.MusicVideoIds)
|
||||||
.Append(request.OtherVideoIds)
|
.Append(request.OtherVideoIds)
|
||||||
|
.Append(request.SongIds)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList();
|
var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList();
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record AddSongToCollection
|
||||||
|
(int CollectionId, int SongId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.Playouts.Commands;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class AddSongToCollectionHandler :
|
||||||
|
MediatR.IRequestHandler<AddSongToCollection, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public AddSongToCollectionHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IMediaCollectionRepository mediaCollectionRepository,
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
_channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
AddSongToCollection request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||||
|
return await validation.Apply(parameters => ApplyAddSongRequest(dbContext, parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyAddSongRequest(TvContext dbContext, Parameters parameters)
|
||||||
|
{
|
||||||
|
parameters.Collection.MediaItems.Add(parameters.Song);
|
||||||
|
if (await dbContext.SaveChangesAsync() > 0)
|
||||||
|
{
|
||||||
|
// rebuild all playouts that use this collection
|
||||||
|
foreach (int playoutId in await _mediaCollectionRepository
|
||||||
|
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||||
|
{
|
||||||
|
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddSongToCollection request) =>
|
||||||
|
(await CollectionMustExist(dbContext, request), await ValidateSong(dbContext, request))
|
||||||
|
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddSongToCollection request) =>
|
||||||
|
dbContext.Collections
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||||
|
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, Song>> ValidateSong(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddSongToCollection request) =>
|
||||||
|
dbContext.Songs
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
|
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
var result2 = Optional(createCollection.Name)
|
var result2 = Optional(createCollection.Name)
|
||||||
.Filter(name => !allNames.Contains(name))
|
.Where(name => !allNames.Contains(name))
|
||||||
.ToValidation<BaseError>("Collection name must be unique");
|
.ToValidation<BaseError>("Collection name must be unique");
|
||||||
|
|
||||||
return (result1, result2).Apply((_, _) => createCollection.Name);
|
return (result1, result2).Apply((_, _) => createCollection.Name);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name));
|
.Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
var result2 = Optional(createMultiCollection.Name)
|
var result2 = Optional(createMultiCollection.Name)
|
||||||
.Filter(name => !allNames.Contains(name))
|
.Where(name => !allNames.Contains(name))
|
||||||
.ToValidation<BaseError>("MultiCollection name must be unique");
|
.ToValidation<BaseError>("MultiCollection name must be unique");
|
||||||
|
|
||||||
return (result1, result2).Apply((_, _) => createMultiCollection.Name);
|
return (result1, result2).Apply((_, _) => createMultiCollection.Name);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.Bind(_ => createSmartCollection.NotLongerThan(50)(c => c.Name));
|
.Bind(_ => createSmartCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
var result2 = Optional(createSmartCollection.Name)
|
var result2 = Optional(createSmartCollection.Name)
|
||||||
.Filter(name => !allNames.Contains(name))
|
.Where(name => !allNames.Contains(name))
|
||||||
.ToValidation<BaseError>("SmartCollection name must be unique");
|
.ToValidation<BaseError>("SmartCollection name must be unique");
|
||||||
|
|
||||||
return (result1, result2).Apply((_, _) => createSmartCollection.Name);
|
return (result1, result2).Apply((_, _) => createSmartCollection.Name);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
Validation<BaseError, TraktList> validation = await TraktListMustExist(dbContext, request.TraktListId);
|
Validation<BaseError, TraktList> validation = await TraktListMustExist(dbContext, request.TraktListId);
|
||||||
return await validation.Match(
|
return await validation.Match(
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
var guids = item.Guids.Map(g => g.Guid).ToList();
|
var guids = item.Guids.Map(g => g.Guid).ToList();
|
||||||
|
|
||||||
Option<int> maybeMovieByGuid = await dbContext.MovieMetadata
|
Option<int> maybeMovieByGuid = await dbContext.MovieMetadata
|
||||||
|
.AsNoTracking()
|
||||||
.Filter(mm => mm.Guids.Any(g => guids.Contains(g.Guid)))
|
.Filter(mm => mm.Guids.Any(g => guids.Contains(g.Guid)))
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync()
|
||||||
.Map(Optional)
|
.Map(Optional)
|
||||||
@@ -220,6 +221,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
Option<int> maybeMovieByTitleYear = await dbContext.MovieMetadata
|
Option<int> maybeMovieByTitleYear = await dbContext.MovieMetadata
|
||||||
|
.AsNoTracking()
|
||||||
.Filter(mm => mm.Title == item.Title && mm.Year == item.Year)
|
.Filter(mm => mm.Title == item.Title && mm.Year == item.Year)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync()
|
||||||
.Map(Optional)
|
.Map(Optional)
|
||||||
@@ -241,6 +243,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
var guids = item.Guids.Map(g => g.Guid).ToList();
|
var guids = item.Guids.Map(g => g.Guid).ToList();
|
||||||
|
|
||||||
Option<int> maybeShowByGuid = await dbContext.ShowMetadata
|
Option<int> maybeShowByGuid = await dbContext.ShowMetadata
|
||||||
|
.AsNoTracking()
|
||||||
.Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid)))
|
.Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid)))
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync()
|
||||||
.Map(Optional)
|
.Map(Optional)
|
||||||
@@ -253,6 +256,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
Option<int> maybeShowByTitleYear = await dbContext.ShowMetadata
|
Option<int> maybeShowByTitleYear = await dbContext.ShowMetadata
|
||||||
|
.AsNoTracking()
|
||||||
.Filter(sm => sm.Title == item.Title && sm.Year == item.Year)
|
.Filter(sm => sm.Title == item.Title && sm.Year == item.Year)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync()
|
||||||
.Map(Optional)
|
.Map(Optional)
|
||||||
@@ -274,6 +278,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
var guids = item.Guids.Map(g => g.Guid).ToList();
|
var guids = item.Guids.Map(g => g.Guid).ToList();
|
||||||
|
|
||||||
Option<int> maybeSeasonByGuid = await dbContext.SeasonMetadata
|
Option<int> maybeSeasonByGuid = await dbContext.SeasonMetadata
|
||||||
|
.AsNoTracking()
|
||||||
.Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid)))
|
.Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid)))
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync()
|
||||||
.Map(Optional)
|
.Map(Optional)
|
||||||
@@ -286,6 +291,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
Option<int> maybeSeasonByTitleYear = await dbContext.SeasonMetadata
|
Option<int> maybeSeasonByTitleYear = await dbContext.SeasonMetadata
|
||||||
|
.AsNoTracking()
|
||||||
.Filter(sm => sm.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year))
|
.Filter(sm => sm.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year))
|
||||||
.Filter(sm => sm.Season.SeasonNumber == item.Season)
|
.Filter(sm => sm.Season.SeasonNumber == item.Season)
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync()
|
||||||
@@ -308,6 +314,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
var guids = item.Guids.Map(g => g.Guid).ToList();
|
var guids = item.Guids.Map(g => g.Guid).ToList();
|
||||||
|
|
||||||
Option<int> maybeEpisodeByGuid = await dbContext.EpisodeMetadata
|
Option<int> maybeEpisodeByGuid = await dbContext.EpisodeMetadata
|
||||||
|
.AsNoTracking()
|
||||||
.Filter(em => em.Guids.Any(g => guids.Contains(g.Guid)))
|
.Filter(em => em.Guids.Any(g => guids.Contains(g.Guid)))
|
||||||
.FirstOrDefaultAsync()
|
.FirstOrDefaultAsync()
|
||||||
.Map(Optional)
|
.Map(Optional)
|
||||||
@@ -320,6 +327,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
Option<int> maybeEpisodeByTitleYear = await dbContext.EpisodeMetadata
|
Option<int> maybeEpisodeByTitleYear = await dbContext.EpisodeMetadata
|
||||||
|
.AsNoTracking()
|
||||||
.Filter(sm => sm.Episode.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year))
|
.Filter(sm => sm.Episode.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year))
|
||||||
.Filter(em => em.Episode.Season.SeasonNumber == item.Season)
|
.Filter(em => em.Episode.Season.SeasonNumber == item.Season)
|
||||||
.Filter(sm => sm.Episode.EpisodeMetadata.Any(e => e.EpisodeNumber == item.Episode))
|
.Filter(sm => sm.Episode.EpisodeMetadata.Any(e => e.EpisodeNumber == item.Episode))
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.Bind(_ => updateMultiCollection.NotLongerThan(50)(c => c.Name));
|
.Bind(_ => updateMultiCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
var result2 = Optional(updateMultiCollection.Name)
|
var result2 = Optional(updateMultiCollection.Name)
|
||||||
.Filter(name => !allNames.Contains(name))
|
.Where(name => !allNames.Contains(name))
|
||||||
.ToValidation<BaseError>("MultiCollection name must be unique");
|
.ToValidation<BaseError>("MultiCollection name must be unique");
|
||||||
|
|
||||||
return (result1, result2).Apply((_, _) => updateMultiCollection.Name);
|
return (result1, result2).Apply((_, _) => updateMultiCollection.Name);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace ErsatzTV.Application.MediaCollections
|
|||||||
internal static class Mapper
|
internal static class Mapper
|
||||||
{
|
{
|
||||||
internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) =>
|
internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) =>
|
||||||
new(collection.Id, collection.Name, collection.UseCustomPlaybackOrder);
|
new(collection.Id, collection.Name, collection.UseCustomPlaybackOrder, MediaItemState.Normal);
|
||||||
|
|
||||||
internal static MultiCollectionViewModel ProjectToViewModel(MultiCollection multiCollection) =>
|
internal static MultiCollectionViewModel ProjectToViewModel(MultiCollection multiCollection) =>
|
||||||
new(
|
new(
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
using ErsatzTV.Application.MediaCards;
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections
|
namespace ErsatzTV.Application.MediaCollections
|
||||||
{
|
{
|
||||||
public record MediaCollectionViewModel(int Id, string Name, bool UseCustomPlaybackOrder) : MediaCardViewModel(
|
public record MediaCollectionViewModel(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
bool UseCustomPlaybackOrder,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
Id,
|
Id,
|
||||||
Name,
|
Name,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
Name,
|
Name,
|
||||||
string.Empty);
|
string.Empty,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
private readonly IMovieFolderScanner _movieFolderScanner;
|
private readonly IMovieFolderScanner _movieFolderScanner;
|
||||||
private readonly IMusicVideoFolderScanner _musicVideoFolderScanner;
|
private readonly IMusicVideoFolderScanner _musicVideoFolderScanner;
|
||||||
private readonly IOtherVideoFolderScanner _otherVideoFolderScanner;
|
private readonly IOtherVideoFolderScanner _otherVideoFolderScanner;
|
||||||
|
private readonly ISongFolderScanner _songFolderScanner;
|
||||||
private readonly ITelevisionFolderScanner _televisionFolderScanner;
|
private readonly ITelevisionFolderScanner _televisionFolderScanner;
|
||||||
|
|
||||||
public ScanLocalLibraryHandler(
|
public ScanLocalLibraryHandler(
|
||||||
@@ -36,6 +37,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
ITelevisionFolderScanner televisionFolderScanner,
|
ITelevisionFolderScanner televisionFolderScanner,
|
||||||
IMusicVideoFolderScanner musicVideoFolderScanner,
|
IMusicVideoFolderScanner musicVideoFolderScanner,
|
||||||
IOtherVideoFolderScanner otherVideoFolderScanner,
|
IOtherVideoFolderScanner otherVideoFolderScanner,
|
||||||
|
ISongFolderScanner songFolderScanner,
|
||||||
IEntityLocker entityLocker,
|
IEntityLocker entityLocker,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
ILogger<ScanLocalLibraryHandler> logger)
|
ILogger<ScanLocalLibraryHandler> logger)
|
||||||
@@ -46,6 +48,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
_televisionFolderScanner = televisionFolderScanner;
|
_televisionFolderScanner = televisionFolderScanner;
|
||||||
_musicVideoFolderScanner = musicVideoFolderScanner;
|
_musicVideoFolderScanner = musicVideoFolderScanner;
|
||||||
_otherVideoFolderScanner = otherVideoFolderScanner;
|
_otherVideoFolderScanner = otherVideoFolderScanner;
|
||||||
|
_songFolderScanner = songFolderScanner;
|
||||||
_entityLocker = entityLocker;
|
_entityLocker = entityLocker;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -67,7 +70,8 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
|
|
||||||
private async Task<Unit> PerformScan(RequestParameters parameters)
|
private async Task<Unit> PerformScan(RequestParameters parameters)
|
||||||
{
|
{
|
||||||
(LocalLibrary localLibrary, string ffprobePath, bool forceScan, int libraryRefreshInterval) = parameters;
|
(LocalLibrary localLibrary, string ffprobePath, string ffmpegPath, bool forceScan,
|
||||||
|
int libraryRefreshInterval) = parameters;
|
||||||
|
|
||||||
var sw = new Stopwatch();
|
var sw = new Stopwatch();
|
||||||
sw.Start();
|
sw.Start();
|
||||||
@@ -117,6 +121,14 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
progressMin,
|
progressMin,
|
||||||
progressMax);
|
progressMax);
|
||||||
break;
|
break;
|
||||||
|
case LibraryMediaKind.Songs:
|
||||||
|
await _songFolderScanner.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
ffprobePath,
|
||||||
|
ffmpegPath,
|
||||||
|
progressMin,
|
||||||
|
progressMax);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryPath.LastScan = DateTime.UtcNow;
|
libraryPath.LastScan = DateTime.UtcNow;
|
||||||
@@ -148,14 +160,36 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalLibrary request) =>
|
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalLibrary request)
|
||||||
(await LocalLibraryMustExist(request), await ValidateFFprobePath(), await ValidateLibraryRefreshInterval())
|
{
|
||||||
|
Validation<BaseError, LocalLibrary> libraryResult = await LocalLibraryMustExist(request);
|
||||||
|
Validation<BaseError, string> ffprobePathResult = await ValidateFFprobePath();
|
||||||
|
Validation<BaseError, string> ffmpegPathResult = await ValidateFFmpegPath();
|
||||||
|
Validation<BaseError, int> refreshIntervalResult = await ValidateLibraryRefreshInterval();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (libraryResult, ffprobePathResult, ffmpegPathResult, refreshIntervalResult)
|
||||||
.Apply(
|
.Apply(
|
||||||
(library, ffprobePath, libraryRefreshInterval) => new RequestParameters(
|
(library, ffprobePath, ffmpegPath, libraryRefreshInterval) => new RequestParameters(
|
||||||
library,
|
library,
|
||||||
ffprobePath,
|
ffprobePath,
|
||||||
|
ffmpegPath,
|
||||||
request.ForceScan,
|
request.ForceScan,
|
||||||
libraryRefreshInterval));
|
libraryRefreshInterval));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// ensure we unlock the library if any validation is unsuccessful
|
||||||
|
foreach (LocalLibrary library in libraryResult.SuccessToSeq())
|
||||||
|
{
|
||||||
|
if (ffprobePathResult.IsFail || ffmpegPathResult.IsFail || refreshIntervalResult.IsFail)
|
||||||
|
{
|
||||||
|
_entityLocker.UnlockLibrary(library.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
|
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
|
||||||
IScanLocalLibrary request) =>
|
IScanLocalLibrary request) =>
|
||||||
@@ -170,6 +204,13 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
ffprobePath =>
|
ffprobePath =>
|
||||||
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
|
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, string>> ValidateFFmpegPath() =>
|
||||||
|
_configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath)
|
||||||
|
.FilterT(File.Exists)
|
||||||
|
.Map(
|
||||||
|
ffmpegPath =>
|
||||||
|
ffmpegPath.ToValidation<BaseError>("FFmpeg path does not exist on the file system"));
|
||||||
|
|
||||||
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
|
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
|
||||||
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
|
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
|
||||||
.FilterT(lri => lri > 0)
|
.FilterT(lri => lri > 0)
|
||||||
@@ -178,6 +219,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
private record RequestParameters(
|
private record RequestParameters(
|
||||||
LocalLibrary LocalLibrary,
|
LocalLibrary LocalLibrary,
|
||||||
string FFprobePath,
|
string FFprobePath,
|
||||||
|
string FFmpegPath,
|
||||||
bool ForceScan,
|
bool ForceScan,
|
||||||
int LibraryRefreshInterval);
|
int LibraryRefreshInterval);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Emby;
|
using ErsatzTV.Core.Emby;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
using ErsatzTV.Core.Jellyfin;
|
using ErsatzTV.Core.Jellyfin;
|
||||||
using Flurl;
|
using Flurl;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -34,7 +35,9 @@ namespace ErsatzTV.Application.Movies
|
|||||||
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))
|
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
metadata.Directors.Map(d => d.Name).ToList(),
|
metadata.Directors.Map(d => d.Name).ToList(),
|
||||||
metadata.Writers.Map(w => w.Name).ToList())
|
metadata.Writers.Map(w => w.Name).ToList(),
|
||||||
|
movie.GetHeadVersion().MediaFiles.Head().Path,
|
||||||
|
movie.State)
|
||||||
{
|
{
|
||||||
Poster = Artwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby),
|
Poster = Artwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby),
|
||||||
FanArt = Artwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby)
|
FanArt = Artwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using ErsatzTV.Application.MediaCards;
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Movies
|
namespace ErsatzTV.Application.Movies
|
||||||
{
|
{
|
||||||
@@ -15,7 +16,9 @@ namespace ErsatzTV.Application.Movies
|
|||||||
List<CultureInfo> Languages,
|
List<CultureInfo> Languages,
|
||||||
List<ActorCardViewModel> Actors,
|
List<ActorCardViewModel> Actors,
|
||||||
List<string> Directors,
|
List<string> Directors,
|
||||||
List<string> Writers)
|
List<string> Writers,
|
||||||
|
string Path,
|
||||||
|
MediaItemState MediaItemState)
|
||||||
{
|
{
|
||||||
public string Poster { get; set; }
|
public string Poster { get; set; }
|
||||||
public string FanArt { get; set; }
|
public string FanArt { get; set; }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace ErsatzTV.Application.Movies.Queries
|
|||||||
GetMovieById request,
|
GetMovieById request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||||
.Map(list => list.HeadOrNone());
|
.Map(list => list.HeadOrNone());
|
||||||
|
|||||||
@@ -52,6 +52,21 @@ namespace ErsatzTV.Application.Playouts.Commands
|
|||||||
.Include(p => p.ProgramSchedule)
|
.Include(p => p.ProgramSchedule)
|
||||||
.ThenInclude(ps => ps.Items)
|
.ThenInclude(ps => ps.Items)
|
||||||
.ThenInclude(psi => psi.MediaItem)
|
.ThenInclude(psi => psi.MediaItem)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.PreRollFiller)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.MidRollFiller)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.PostRollFiller)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.TailFiller)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.FallbackFiller)
|
||||||
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId)
|
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId)
|
||||||
.Map(o => o.ToValidation<BaseError>("Playout does not exist."));
|
.Map(o => o.ToValidation<BaseError>("Playout does not exist."));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ namespace ErsatzTV.Application.Playouts
|
|||||||
{
|
{
|
||||||
internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
|
internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
|
||||||
new(
|
new(
|
||||||
GetDisplayTitle(playoutItem.MediaItem),
|
GetDisplayTitle(playoutItem),
|
||||||
playoutItem.StartOffset,
|
playoutItem.StartOffset,
|
||||||
GetDisplayDuration(playoutItem.MediaItem));
|
GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset));
|
||||||
|
|
||||||
private static string GetDisplayTitle(MediaItem mediaItem)
|
private static string GetDisplayTitle(PlayoutItem playoutItem)
|
||||||
{
|
{
|
||||||
switch (mediaItem)
|
switch (playoutItem.MediaItem)
|
||||||
{
|
{
|
||||||
case Episode e:
|
case Episode e:
|
||||||
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
|
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
|
||||||
@@ -28,6 +28,11 @@ namespace ErsatzTV.Application.Playouts
|
|||||||
|
|
||||||
var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}";
|
var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}";
|
||||||
var titlesString = $"{string.Join('/', episodeTitles)}";
|
var titlesString = $"{string.Join('/', episodeTitles)}";
|
||||||
|
if (!string.IsNullOrWhiteSpace(playoutItem.ChapterTitle))
|
||||||
|
{
|
||||||
|
titlesString += $" ({playoutItem.ChapterTitle})";
|
||||||
|
}
|
||||||
|
|
||||||
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
|
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
|
||||||
case Movie m:
|
case Movie m:
|
||||||
return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]");
|
return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]");
|
||||||
@@ -36,29 +41,29 @@ namespace ErsatzTV.Application.Playouts
|
|||||||
.Map(am => $"{am.Title} - ").IfNone(string.Empty);
|
.Map(am => $"{am.Title} - ").IfNone(string.Empty);
|
||||||
return mv.MusicVideoMetadata.HeadOrNone()
|
return mv.MusicVideoMetadata.HeadOrNone()
|
||||||
.Map(mvm => $"{artistName}{mvm.Title}")
|
.Map(mvm => $"{artistName}{mvm.Title}")
|
||||||
|
.Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? s : $"{s} ({playoutItem.ChapterTitle})")
|
||||||
.IfNone("[unknown music video]");
|
.IfNone("[unknown music video]");
|
||||||
case OtherVideo ov:
|
case OtherVideo ov:
|
||||||
return ov.OtherVideoMetadata.HeadOrNone().Map(ovm => ovm.Title ?? string.Empty)
|
return ov.OtherVideoMetadata.HeadOrNone()
|
||||||
|
.Map(ovm => ovm.Title ?? string.Empty)
|
||||||
|
.Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? s : $"{s} ({playoutItem.ChapterTitle})")
|
||||||
.IfNone("[unknown video]");
|
.IfNone("[unknown video]");
|
||||||
|
case Song s:
|
||||||
|
string songArtist = s.SongMetadata.HeadOrNone()
|
||||||
|
.Map(sm => string.IsNullOrWhiteSpace(sm.Artist) ? string.Empty : $"{sm.Artist} - ")
|
||||||
|
.IfNone(string.Empty);
|
||||||
|
return s.SongMetadata.HeadOrNone()
|
||||||
|
.Map(sm => $"{songArtist}{sm.Title ?? string.Empty}")
|
||||||
|
.Map(t => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? t : $"{s} ({playoutItem.ChapterTitle})")
|
||||||
|
.IfNone("[unknown song]");
|
||||||
default:
|
default:
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetDisplayDuration(MediaItem mediaItem)
|
private static string GetDisplayDuration(TimeSpan duration) =>
|
||||||
{
|
string.Format(
|
||||||
MediaVersion version = mediaItem switch
|
duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||||
{
|
duration);
|
||||||
Movie m => m.MediaVersions.Head(),
|
|
||||||
Episode e => e.MediaVersions.Head(),
|
|
||||||
MusicVideo mv => mv.MediaVersions.Head(),
|
|
||||||
OtherVideo ov => ov.MediaVersions.Head(),
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
|
||||||
};
|
|
||||||
|
|
||||||
return string.Format(
|
|
||||||
version.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
|
||||||
version.Duration);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using MediatR;
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Playouts.Queries
|
namespace ErsatzTV.Application.Playouts.Queries
|
||||||
{
|
{
|
||||||
public record GetFuturePlayoutItemsById(int PlayoutId, int PageNum, int PageSize) : IRequest<PagedPlayoutItemsViewModel>;
|
public record GetFuturePlayoutItemsById(int PlayoutId, bool ShowFiller, int PageNum, int PageSize) : IRequest<PagedPlayoutItemsViewModel>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -23,13 +24,13 @@ namespace ErsatzTV.Application.Playouts.Queries
|
|||||||
GetFuturePlayoutItemsById request,
|
GetFuturePlayoutItemsById request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
int totalCount = await dbContext.PlayoutItems
|
|
||||||
.CountAsync(i => i.PlayoutId == request.PlayoutId, cancellationToken);
|
|
||||||
|
|
||||||
DateTime now = DateTimeOffset.Now.UtcDateTime;
|
DateTime now = DateTimeOffset.Now.UtcDateTime;
|
||||||
|
|
||||||
|
int totalCount = await dbContext.PlayoutItems
|
||||||
|
.CountAsync(i => i.Finish >= now && i.PlayoutId == request.PlayoutId && (request.ShowFiller || i.FillerKind == FillerKind.None), cancellationToken);
|
||||||
|
|
||||||
List<PlayoutItemViewModel> page = await dbContext.PlayoutItems
|
List<PlayoutItemViewModel> page = await dbContext.PlayoutItems
|
||||||
.Include(i => i.MediaItem)
|
.Include(i => i.MediaItem)
|
||||||
.ThenInclude(mi => (mi as Movie).MovieMetadata)
|
.ThenInclude(mi => (mi as Movie).MovieMetadata)
|
||||||
@@ -56,8 +57,13 @@ namespace ErsatzTV.Application.Playouts.Queries
|
|||||||
.ThenInclude(mi => (mi as OtherVideo).OtherVideoMetadata)
|
.ThenInclude(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||||
.Include(i => i.MediaItem)
|
.Include(i => i.MediaItem)
|
||||||
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
|
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
|
||||||
|
.Include(i => i.MediaItem)
|
||||||
|
.ThenInclude(mi => (mi as Song).SongMetadata)
|
||||||
|
.Include(i => i.MediaItem)
|
||||||
|
.ThenInclude(mi => (mi as Song).MediaVersions)
|
||||||
.Filter(i => i.PlayoutId == request.PlayoutId)
|
.Filter(i => i.PlayoutId == request.PlayoutId)
|
||||||
.Filter(i => i.Finish >= now)
|
.Filter(i => i.Finish >= now)
|
||||||
|
.Filter(i => request.ShowFiller || i.FillerKind == FillerKind.None)
|
||||||
.OrderBy(i => i.Start)
|
.OrderBy(i => i.Start)
|
||||||
.Skip(request.PageNum * request.PageSize)
|
.Skip(request.PageNum * request.PageSize)
|
||||||
.Take(request.PageSize)
|
.Take(request.PageSize)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain;
|
|||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using ErsatzTV.Core.Interfaces.Plex;
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Plex;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -23,16 +24,22 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
private readonly ILogger<SynchronizePlexMediaSourcesHandler> _logger;
|
private readonly ILogger<SynchronizePlexMediaSourcesHandler> _logger;
|
||||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
private readonly IPlexTvApiClient _plexTvApiClient;
|
private readonly IPlexTvApiClient _plexTvApiClient;
|
||||||
|
private readonly IPlexServerApiClient _plexServerApiClient;
|
||||||
|
private readonly IPlexSecretStore _plexSecretStore;
|
||||||
|
|
||||||
public SynchronizePlexMediaSourcesHandler(
|
public SynchronizePlexMediaSourcesHandler(
|
||||||
IMediaSourceRepository mediaSourceRepository,
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
IPlexTvApiClient plexTvApiClient,
|
IPlexTvApiClient plexTvApiClient,
|
||||||
|
IPlexServerApiClient plexServerApiClient,
|
||||||
|
IPlexSecretStore plexSecretStore,
|
||||||
ChannelWriter<IPlexBackgroundServiceRequest> channel,
|
ChannelWriter<IPlexBackgroundServiceRequest> channel,
|
||||||
IEntityLocker entityLocker,
|
IEntityLocker entityLocker,
|
||||||
ILogger<SynchronizePlexMediaSourcesHandler> logger)
|
ILogger<SynchronizePlexMediaSourcesHandler> logger)
|
||||||
{
|
{
|
||||||
_mediaSourceRepository = mediaSourceRepository;
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
_plexTvApiClient = plexTvApiClient;
|
_plexTvApiClient = plexTvApiClient;
|
||||||
|
_plexServerApiClient = plexServerApiClient;
|
||||||
|
_plexSecretStore = plexSecretStore;
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
_entityLocker = entityLocker;
|
_entityLocker = entityLocker;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -69,12 +76,12 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
return allExisting;
|
return allExisting;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
private async Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
||||||
{
|
{
|
||||||
Option<PlexMediaSource> maybeExisting =
|
Option<PlexMediaSource> maybeExisting =
|
||||||
allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier);
|
allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier);
|
||||||
return maybeExisting.Match(
|
|
||||||
existing =>
|
foreach (PlexMediaSource existing in maybeExisting)
|
||||||
{
|
{
|
||||||
existing.Platform = server.Platform;
|
existing.Platform = server.Platform;
|
||||||
existing.PlatformVersion = server.PlatformVersion;
|
existing.PlatformVersion = server.PlatformVersion;
|
||||||
@@ -84,17 +91,61 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||||
var toRemove = existing.Connections
|
var toRemove = existing.Connections
|
||||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||||
return _mediaSourceRepository.Update(existing, server.Connections, toAdd, toRemove);
|
await _mediaSourceRepository.Update(existing, toAdd, toRemove);
|
||||||
},
|
await FindConnectionToActivate(existing);
|
||||||
async () =>
|
}
|
||||||
|
|
||||||
|
if (maybeExisting.IsNone)
|
||||||
{
|
{
|
||||||
if (server.Connections.Any())
|
await _mediaSourceRepository.Add(server);
|
||||||
|
await FindConnectionToActivate(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FindConnectionToActivate(PlexMediaSource server)
|
||||||
{
|
{
|
||||||
|
var prioritized = server.Connections.OrderBy(pc => pc.IsActive ? 0 : 1).ToList();
|
||||||
|
foreach (PlexConnection connection in server.Connections)
|
||||||
|
{
|
||||||
|
connection.IsActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Option<PlexServerAuthToken> maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier);
|
||||||
|
foreach (PlexServerAuthToken token in maybeToken)
|
||||||
|
{
|
||||||
|
foreach (PlexConnection connection in prioritized)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri);
|
||||||
|
if (await _plexServerApiClient.Ping(connection, token))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Located Plex at {Uri}", connection.Uri);
|
||||||
|
connection.IsActive = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybeToken.IsNone)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Unable to activate Plex connection for server {Server} without auth token",
|
||||||
|
server.ServerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.Connections.All(c => !c.IsActive))
|
||||||
|
{
|
||||||
|
_logger.LogError("Unable to locate Plex");
|
||||||
server.Connections.Head().IsActive = true;
|
server.Connections.Head().IsActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _mediaSourceRepository.Add(server);
|
await _mediaSourceRepository.Update(server, new List<PlexConnection>(), new List<PlexConnection>());
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
int? MultipleCount,
|
int? MultipleCount,
|
||||||
TimeSpan? PlayoutDuration,
|
TimeSpan? PlayoutDuration,
|
||||||
TailMode TailMode,
|
TailMode TailMode,
|
||||||
ProgramScheduleItemCollectionType TailCollectionType,
|
|
||||||
int? TailCollectionId,
|
|
||||||
int? TailMultiCollectionId,
|
|
||||||
int? TailSmartCollectionId,
|
|
||||||
int? TailMediaItemId,
|
|
||||||
string CustomTitle,
|
string CustomTitle,
|
||||||
GuideMode GuideMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
GuideMode GuideMode,
|
||||||
|
int? PreRollFillerId,
|
||||||
|
int? MidRollFillerId,
|
||||||
|
int? PostRollFillerId,
|
||||||
|
int? TailFillerId,
|
||||||
|
int? FallbackFillerId) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
.CountAsync(ps => ps.Name == createProgramSchedule.Name);
|
.CountAsync(ps => ps.Name == createProgramSchedule.Name);
|
||||||
|
|
||||||
var result2 = Optional(duplicateNameCount)
|
var result2 = Optional(duplicateNameCount)
|
||||||
.Filter(count => count == 0)
|
.Where(count => count == 0)
|
||||||
.ToValidation<BaseError>("Schedule name must be unique");
|
.ToValidation<BaseError>("Schedule name must be unique");
|
||||||
|
|
||||||
return (result1, result2).Apply((_, _) => createProgramSchedule.Name);
|
return (result1, result2).Apply((_, _) => createProgramSchedule.Name);
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
int? MultipleCount { get; }
|
int? MultipleCount { get; }
|
||||||
TimeSpan? PlayoutDuration { get; }
|
TimeSpan? PlayoutDuration { get; }
|
||||||
TailMode TailMode { get; }
|
TailMode TailMode { get; }
|
||||||
ProgramScheduleItemCollectionType TailCollectionType { get; }
|
|
||||||
int? TailCollectionId { get; }
|
|
||||||
int? TailMultiCollectionId { get; }
|
|
||||||
int? TailSmartCollectionId { get; }
|
|
||||||
int? TailMediaItemId { get; }
|
|
||||||
string CustomTitle { get; }
|
string CustomTitle { get; }
|
||||||
GuideMode GuideMode { get; }
|
GuideMode GuideMode { get; }
|
||||||
|
int? PreRollFillerId { get; }
|
||||||
|
int? MidRollFillerId { get; }
|
||||||
|
int? PostRollFillerId { get; }
|
||||||
|
int? TailFillerId { get; }
|
||||||
|
int? FallbackFillerId { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||||
{
|
{
|
||||||
@@ -20,6 +24,33 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
.SelectOneAsync(ps => ps.Id, ps => ps.Id == programScheduleId)
|
.SelectOneAsync(ps => ps.Id, ps => ps.Id == programScheduleId)
|
||||||
.Map(o => o.ToValidation<BaseError>("[ProgramScheduleId] does not exist."));
|
.Map(o => o.ToValidation<BaseError>("[ProgramScheduleId] does not exist."));
|
||||||
|
|
||||||
|
protected static async Task<Either<BaseError, ProgramSchedule>> FillerConfigurationMustBeValid(
|
||||||
|
TvContext dbContext,
|
||||||
|
IProgramScheduleItemRequest item,
|
||||||
|
ProgramSchedule programSchedule)
|
||||||
|
{
|
||||||
|
var allFillerIds = Optional(item.PreRollFillerId)
|
||||||
|
.Append(Optional(item.MidRollFillerId))
|
||||||
|
.Append(Optional(item.PostRollFillerId))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
List<FillerPreset> allFiller = await dbContext.FillerPresets
|
||||||
|
.Filter(fp => allFillerIds.Contains(fp.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1)
|
||||||
|
{
|
||||||
|
return BaseError.New("Schedule may only contain one filler preset that is configured to pad");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allFiller.Any(fp => fp.PadToNearestMinute.HasValue) && !item.FallbackFillerId.HasValue)
|
||||||
|
{
|
||||||
|
return BaseError.New("Fallback filler is required when padding");
|
||||||
|
}
|
||||||
|
|
||||||
|
return programSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
protected static Validation<BaseError, ProgramSchedule> PlayoutModeMustBeValid(
|
protected static Validation<BaseError, ProgramSchedule> PlayoutModeMustBeValid(
|
||||||
IProgramScheduleItemRequest item,
|
IProgramScheduleItemRequest item,
|
||||||
ProgramSchedule programSchedule)
|
ProgramSchedule programSchedule)
|
||||||
@@ -55,6 +86,16 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
return BaseError.New("[PlayoutDuration] is required for playout mode 'duration'");
|
return BaseError.New("[PlayoutDuration] is required for playout mode 'duration'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.TailMode == TailMode.Filler && item.TailFillerId == null)
|
||||||
|
{
|
||||||
|
return BaseError.New("Tail Filler is required with tail mode Filler");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.TailFillerId != null && item.TailMode != TailMode.Filler)
|
||||||
|
{
|
||||||
|
return BaseError.New("Tail Filler will not be used unless tail mode is set to Filler");
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return BaseError.New("[PlayoutMode] is invalid");
|
return BaseError.New("[PlayoutMode] is invalid");
|
||||||
@@ -128,7 +169,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
{
|
{
|
||||||
ProgramScheduleId = programSchedule.Id,
|
ProgramScheduleId = programSchedule.Id,
|
||||||
Index = index,
|
Index = index,
|
||||||
StartTime = item.StartTime,
|
StartTime = FixStartTime(item.StartTime),
|
||||||
CollectionType = item.CollectionType,
|
CollectionType = item.CollectionType,
|
||||||
CollectionId = item.CollectionId,
|
CollectionId = item.CollectionId,
|
||||||
MultiCollectionId = item.MultiCollectionId,
|
MultiCollectionId = item.MultiCollectionId,
|
||||||
@@ -136,13 +177,18 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
MediaItemId = item.MediaItemId,
|
MediaItemId = item.MediaItemId,
|
||||||
PlaybackOrder = item.PlaybackOrder,
|
PlaybackOrder = item.PlaybackOrder,
|
||||||
CustomTitle = item.CustomTitle,
|
CustomTitle = item.CustomTitle,
|
||||||
GuideMode = item.GuideMode
|
GuideMode = item.GuideMode,
|
||||||
|
PreRollFillerId = item.PreRollFillerId,
|
||||||
|
MidRollFillerId = item.MidRollFillerId,
|
||||||
|
PostRollFillerId = item.PostRollFillerId,
|
||||||
|
TailFillerId = item.TailFillerId,
|
||||||
|
FallbackFillerId = item.FallbackFillerId
|
||||||
},
|
},
|
||||||
PlayoutMode.One => new ProgramScheduleItemOne
|
PlayoutMode.One => new ProgramScheduleItemOne
|
||||||
{
|
{
|
||||||
ProgramScheduleId = programSchedule.Id,
|
ProgramScheduleId = programSchedule.Id,
|
||||||
Index = index,
|
Index = index,
|
||||||
StartTime = item.StartTime,
|
StartTime = FixStartTime(item.StartTime),
|
||||||
CollectionType = item.CollectionType,
|
CollectionType = item.CollectionType,
|
||||||
CollectionId = item.CollectionId,
|
CollectionId = item.CollectionId,
|
||||||
MultiCollectionId = item.MultiCollectionId,
|
MultiCollectionId = item.MultiCollectionId,
|
||||||
@@ -150,13 +196,18 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
MediaItemId = item.MediaItemId,
|
MediaItemId = item.MediaItemId,
|
||||||
PlaybackOrder = item.PlaybackOrder,
|
PlaybackOrder = item.PlaybackOrder,
|
||||||
CustomTitle = item.CustomTitle,
|
CustomTitle = item.CustomTitle,
|
||||||
GuideMode = item.GuideMode
|
GuideMode = item.GuideMode,
|
||||||
|
PreRollFillerId = item.PreRollFillerId,
|
||||||
|
MidRollFillerId = item.MidRollFillerId,
|
||||||
|
PostRollFillerId = item.PostRollFillerId,
|
||||||
|
TailFillerId = item.TailFillerId,
|
||||||
|
FallbackFillerId = item.FallbackFillerId
|
||||||
},
|
},
|
||||||
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
|
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
|
||||||
{
|
{
|
||||||
ProgramScheduleId = programSchedule.Id,
|
ProgramScheduleId = programSchedule.Id,
|
||||||
Index = index,
|
Index = index,
|
||||||
StartTime = item.StartTime,
|
StartTime = FixStartTime(item.StartTime),
|
||||||
CollectionType = item.CollectionType,
|
CollectionType = item.CollectionType,
|
||||||
CollectionId = item.CollectionId,
|
CollectionId = item.CollectionId,
|
||||||
MultiCollectionId = item.MultiCollectionId,
|
MultiCollectionId = item.MultiCollectionId,
|
||||||
@@ -165,13 +216,18 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
PlaybackOrder = item.PlaybackOrder,
|
PlaybackOrder = item.PlaybackOrder,
|
||||||
Count = item.MultipleCount.GetValueOrDefault(),
|
Count = item.MultipleCount.GetValueOrDefault(),
|
||||||
CustomTitle = item.CustomTitle,
|
CustomTitle = item.CustomTitle,
|
||||||
GuideMode = item.GuideMode
|
GuideMode = item.GuideMode,
|
||||||
|
PreRollFillerId = item.PreRollFillerId,
|
||||||
|
MidRollFillerId = item.MidRollFillerId,
|
||||||
|
PostRollFillerId = item.PostRollFillerId,
|
||||||
|
TailFillerId = item.TailFillerId,
|
||||||
|
FallbackFillerId = item.FallbackFillerId
|
||||||
},
|
},
|
||||||
PlayoutMode.Duration => new ProgramScheduleItemDuration
|
PlayoutMode.Duration => new ProgramScheduleItemDuration
|
||||||
{
|
{
|
||||||
ProgramScheduleId = programSchedule.Id,
|
ProgramScheduleId = programSchedule.Id,
|
||||||
Index = index,
|
Index = index,
|
||||||
StartTime = item.StartTime,
|
StartTime = FixStartTime(item.StartTime),
|
||||||
CollectionType = item.CollectionType,
|
CollectionType = item.CollectionType,
|
||||||
CollectionId = item.CollectionId,
|
CollectionId = item.CollectionId,
|
||||||
MultiCollectionId = item.MultiCollectionId,
|
MultiCollectionId = item.MultiCollectionId,
|
||||||
@@ -180,18 +236,23 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
PlaybackOrder = item.PlaybackOrder,
|
PlaybackOrder = item.PlaybackOrder,
|
||||||
PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()),
|
PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()),
|
||||||
TailMode = item.TailMode,
|
TailMode = item.TailMode,
|
||||||
TailCollectionType = item.TailCollectionType,
|
|
||||||
TailCollectionId = item.TailCollectionId,
|
|
||||||
TailMultiCollectionId = item.TailMultiCollectionId,
|
|
||||||
TailSmartCollectionId = item.TailSmartCollectionId,
|
|
||||||
TailMediaItemId = item.TailMediaItemId,
|
|
||||||
CustomTitle = item.CustomTitle,
|
CustomTitle = item.CustomTitle,
|
||||||
GuideMode = item.GuideMode
|
GuideMode = item.GuideMode,
|
||||||
|
PreRollFillerId = item.PreRollFillerId,
|
||||||
|
MidRollFillerId = item.MidRollFillerId,
|
||||||
|
PostRollFillerId = item.PostRollFillerId,
|
||||||
|
TailFillerId = item.TailFillerId,
|
||||||
|
FallbackFillerId = item.FallbackFillerId
|
||||||
},
|
},
|
||||||
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
|
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
|
||||||
};
|
};
|
||||||
|
|
||||||
private static TimeSpan FixDuration(TimeSpan duration) =>
|
private static TimeSpan FixDuration(TimeSpan duration) =>
|
||||||
duration > TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration;
|
duration >= TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration;
|
||||||
|
|
||||||
|
private static TimeSpan? FixStartTime(TimeSpan? startTime) =>
|
||||||
|
startTime.HasValue && startTime.Value >= TimeSpan.FromDays(1)
|
||||||
|
? startTime.Value.Subtract(TimeSpan.FromDays(1))
|
||||||
|
: startTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
int? MultipleCount,
|
int? MultipleCount,
|
||||||
TimeSpan? PlayoutDuration,
|
TimeSpan? PlayoutDuration,
|
||||||
TailMode TailMode,
|
TailMode TailMode,
|
||||||
ProgramScheduleItemCollectionType TailCollectionType,
|
|
||||||
int? TailCollectionId,
|
|
||||||
int? TailMultiCollectionId,
|
|
||||||
int? TailSmartCollectionId,
|
|
||||||
int? TailMediaItemId,
|
|
||||||
string CustomTitle,
|
string CustomTitle,
|
||||||
GuideMode GuideMode) : IProgramScheduleItemRequest;
|
GuideMode GuideMode,
|
||||||
|
int? PreRollFillerId,
|
||||||
|
int? MidRollFillerId,
|
||||||
|
int? PostRollFillerId,
|
||||||
|
int? TailFillerId,
|
||||||
|
int? FallbackFillerId) : IProgramScheduleItemRequest;
|
||||||
|
|
||||||
public record ReplaceProgramScheduleItems
|
public record ReplaceProgramScheduleItems
|
||||||
(int ProgramScheduleId, List<ReplaceProgramScheduleItem> Items) : IRequest<
|
(int ProgramScheduleId, List<ReplaceProgramScheduleItem> Items) : IRequest<
|
||||||
|
|||||||
+22
-1
@@ -63,7 +63,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
ProgramScheduleMustExist(dbContext, request.ProgramScheduleId)
|
ProgramScheduleMustExist(dbContext, request.ProgramScheduleId)
|
||||||
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule))
|
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule))
|
||||||
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule))
|
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule))
|
||||||
.BindT(programSchedule => PlaybackOrdersMustBeValid(request, programSchedule));
|
.BindT(programSchedule => PlaybackOrdersMustBeValid(request, programSchedule))
|
||||||
|
.BindT(programSchedule => FillerConfigurationsMustBeValid(dbContext, request, programSchedule));
|
||||||
|
|
||||||
private static Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid(
|
private static Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid(
|
||||||
ReplaceProgramScheduleItems request,
|
ReplaceProgramScheduleItems request,
|
||||||
@@ -77,6 +78,26 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
|||||||
request.Items.Map(item => CollectionTypeMustBeValid(item, programSchedule)).Sequence()
|
request.Items.Map(item => CollectionTypeMustBeValid(item, programSchedule)).Sequence()
|
||||||
.Map(_ => programSchedule);
|
.Map(_ => programSchedule);
|
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, ProgramSchedule>> FillerConfigurationsMustBeValid(
|
||||||
|
TvContext dbContext,
|
||||||
|
ReplaceProgramScheduleItems request,
|
||||||
|
ProgramSchedule programSchedule)
|
||||||
|
{
|
||||||
|
foreach (ReplaceProgramScheduleItem item in request.Items)
|
||||||
|
{
|
||||||
|
Either<BaseError, ProgramSchedule> result = await FillerConfigurationMustBeValid(
|
||||||
|
dbContext,
|
||||||
|
item,
|
||||||
|
programSchedule);
|
||||||
|
if (result.IsLeft)
|
||||||
|
{
|
||||||
|
return result.ToValidation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return programSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
private static Validation<BaseError, ProgramSchedule> PlaybackOrdersMustBeValid(
|
private static Validation<BaseError, ProgramSchedule> PlaybackOrdersMustBeValid(
|
||||||
ReplaceProgramScheduleItems request,
|
ReplaceProgramScheduleItems request,
|
||||||
ProgramSchedule programSchedule)
|
ProgramSchedule programSchedule)
|
||||||
|
|||||||
@@ -41,25 +41,23 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
duration.PlaybackOrder,
|
duration.PlaybackOrder,
|
||||||
duration.PlayoutDuration,
|
duration.PlayoutDuration,
|
||||||
duration.TailMode,
|
duration.TailMode,
|
||||||
duration.TailCollectionType,
|
|
||||||
duration.TailCollection != null
|
|
||||||
? MediaCollections.Mapper.ProjectToViewModel(duration.TailCollection)
|
|
||||||
: null,
|
|
||||||
duration.TailMultiCollection != null
|
|
||||||
? MediaCollections.Mapper.ProjectToViewModel(duration.TailMultiCollection)
|
|
||||||
: null,
|
|
||||||
duration.TailSmartCollection != null
|
|
||||||
? MediaCollections.Mapper.ProjectToViewModel(duration.TailSmartCollection)
|
|
||||||
: null,
|
|
||||||
duration.TailMediaItem switch
|
|
||||||
{
|
|
||||||
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
|
||||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
|
||||||
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
|
|
||||||
_ => null
|
|
||||||
},
|
|
||||||
duration.CustomTitle,
|
duration.CustomTitle,
|
||||||
duration.GuideMode),
|
duration.GuideMode,
|
||||||
|
duration.PreRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(duration.PreRollFiller)
|
||||||
|
: null,
|
||||||
|
duration.MidRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(duration.MidRollFiller)
|
||||||
|
: null,
|
||||||
|
duration.PostRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(duration.PostRollFiller)
|
||||||
|
: null,
|
||||||
|
duration.TailFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(duration.TailFiller)
|
||||||
|
: null,
|
||||||
|
duration.FallbackFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(duration.FallbackFiller)
|
||||||
|
: null),
|
||||||
ProgramScheduleItemFlood flood =>
|
ProgramScheduleItemFlood flood =>
|
||||||
new ProgramScheduleItemFloodViewModel(
|
new ProgramScheduleItemFloodViewModel(
|
||||||
flood.Id,
|
flood.Id,
|
||||||
@@ -85,7 +83,22 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
},
|
},
|
||||||
flood.PlaybackOrder,
|
flood.PlaybackOrder,
|
||||||
flood.CustomTitle,
|
flood.CustomTitle,
|
||||||
flood.GuideMode),
|
flood.GuideMode,
|
||||||
|
flood.PreRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(flood.PreRollFiller)
|
||||||
|
: null,
|
||||||
|
flood.MidRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(flood.MidRollFiller)
|
||||||
|
: null,
|
||||||
|
flood.PostRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(flood.PostRollFiller)
|
||||||
|
: null,
|
||||||
|
flood.TailFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(flood.TailFiller)
|
||||||
|
: null,
|
||||||
|
flood.FallbackFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(flood.FallbackFiller)
|
||||||
|
: null),
|
||||||
ProgramScheduleItemMultiple multiple =>
|
ProgramScheduleItemMultiple multiple =>
|
||||||
new ProgramScheduleItemMultipleViewModel(
|
new ProgramScheduleItemMultipleViewModel(
|
||||||
multiple.Id,
|
multiple.Id,
|
||||||
@@ -112,7 +125,22 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
multiple.PlaybackOrder,
|
multiple.PlaybackOrder,
|
||||||
multiple.Count,
|
multiple.Count,
|
||||||
multiple.CustomTitle,
|
multiple.CustomTitle,
|
||||||
multiple.GuideMode),
|
multiple.GuideMode,
|
||||||
|
multiple.PreRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(multiple.PreRollFiller)
|
||||||
|
: null,
|
||||||
|
multiple.MidRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(multiple.MidRollFiller)
|
||||||
|
: null,
|
||||||
|
multiple.PostRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(multiple.PostRollFiller)
|
||||||
|
: null,
|
||||||
|
multiple.TailFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(multiple.TailFiller)
|
||||||
|
: null,
|
||||||
|
multiple.FallbackFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(multiple.FallbackFiller)
|
||||||
|
: null),
|
||||||
ProgramScheduleItemOne one =>
|
ProgramScheduleItemOne one =>
|
||||||
new ProgramScheduleItemOneViewModel(
|
new ProgramScheduleItemOneViewModel(
|
||||||
one.Id,
|
one.Id,
|
||||||
@@ -138,7 +166,22 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
},
|
},
|
||||||
one.PlaybackOrder,
|
one.PlaybackOrder,
|
||||||
one.CustomTitle,
|
one.CustomTitle,
|
||||||
one.GuideMode),
|
one.GuideMode,
|
||||||
|
one.PreRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(one.PreRollFiller)
|
||||||
|
: null,
|
||||||
|
one.MidRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(one.MidRollFiller)
|
||||||
|
: null,
|
||||||
|
one.PostRollFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(one.PostRollFiller)
|
||||||
|
: null,
|
||||||
|
one.TailFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(one.TailFiller)
|
||||||
|
: null,
|
||||||
|
one.FallbackFiller != null
|
||||||
|
? Filler.Mapper.ProjectToViewModel(one.FallbackFiller)
|
||||||
|
: null),
|
||||||
_ => throw new NotSupportedException(
|
_ => throw new NotSupportedException(
|
||||||
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
|
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using ErsatzTV.Application.Filler;
|
||||||
using ErsatzTV.Application.MediaCollections;
|
using ErsatzTV.Application.MediaCollections;
|
||||||
using ErsatzTV.Application.MediaItems;
|
using ErsatzTV.Application.MediaItems;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -20,13 +21,13 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
PlaybackOrder playbackOrder,
|
PlaybackOrder playbackOrder,
|
||||||
TimeSpan playoutDuration,
|
TimeSpan playoutDuration,
|
||||||
TailMode tailMode,
|
TailMode tailMode,
|
||||||
ProgramScheduleItemCollectionType tailCollectionType,
|
|
||||||
MediaCollectionViewModel tailCollection,
|
|
||||||
MultiCollectionViewModel tailMultiCollection,
|
|
||||||
SmartCollectionViewModel tailSmartCollection,
|
|
||||||
NamedMediaItemViewModel tailMediaItem,
|
|
||||||
string customTitle,
|
string customTitle,
|
||||||
GuideMode guideMode) : base(
|
GuideMode guideMode,
|
||||||
|
FillerPresetViewModel preRollFiller,
|
||||||
|
FillerPresetViewModel midRollFiller,
|
||||||
|
FillerPresetViewModel postRollFiller,
|
||||||
|
FillerPresetViewModel tailFiller,
|
||||||
|
FillerPresetViewModel fallbackFiller) : base(
|
||||||
id,
|
id,
|
||||||
index,
|
index,
|
||||||
startType,
|
startType,
|
||||||
@@ -39,25 +40,18 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
mediaItem,
|
mediaItem,
|
||||||
playbackOrder,
|
playbackOrder,
|
||||||
customTitle,
|
customTitle,
|
||||||
guideMode)
|
guideMode,
|
||||||
|
preRollFiller,
|
||||||
|
midRollFiller,
|
||||||
|
postRollFiller,
|
||||||
|
tailFiller,
|
||||||
|
fallbackFiller)
|
||||||
{
|
{
|
||||||
PlayoutDuration = playoutDuration;
|
PlayoutDuration = playoutDuration;
|
||||||
TailMode = tailMode;
|
TailMode = tailMode;
|
||||||
TailCollectionType = tailCollectionType;
|
|
||||||
TailCollection = tailCollection;
|
|
||||||
TailMultiCollection = tailMultiCollection;
|
|
||||||
TailSmartCollection = tailSmartCollection;
|
|
||||||
TailMediaItem = tailMediaItem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TimeSpan PlayoutDuration { get; }
|
public TimeSpan PlayoutDuration { get; }
|
||||||
public TailMode TailMode { get; }
|
public TailMode TailMode { get; }
|
||||||
public ProgramScheduleItemCollectionType TailCollectionType { get; }
|
|
||||||
|
|
||||||
public MediaCollectionViewModel TailCollection { get; }
|
|
||||||
public MultiCollectionViewModel TailMultiCollection { get; }
|
|
||||||
public SmartCollectionViewModel TailSmartCollection { get; }
|
|
||||||
public NamedMediaItemViewModel TailMediaItem { get; }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using ErsatzTV.Application.Filler;
|
||||||
using ErsatzTV.Application.MediaCollections;
|
using ErsatzTV.Application.MediaCollections;
|
||||||
using ErsatzTV.Application.MediaItems;
|
using ErsatzTV.Application.MediaItems;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -19,7 +20,12 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
NamedMediaItemViewModel mediaItem,
|
NamedMediaItemViewModel mediaItem,
|
||||||
PlaybackOrder playbackOrder,
|
PlaybackOrder playbackOrder,
|
||||||
string customTitle,
|
string customTitle,
|
||||||
GuideMode guideMode) : base(
|
GuideMode guideMode,
|
||||||
|
FillerPresetViewModel preRollFiller,
|
||||||
|
FillerPresetViewModel midRollFiller,
|
||||||
|
FillerPresetViewModel postRollFiller,
|
||||||
|
FillerPresetViewModel tailFiller,
|
||||||
|
FillerPresetViewModel fallbackFiller) : base(
|
||||||
id,
|
id,
|
||||||
index,
|
index,
|
||||||
startType,
|
startType,
|
||||||
@@ -32,7 +38,12 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
mediaItem,
|
mediaItem,
|
||||||
playbackOrder,
|
playbackOrder,
|
||||||
customTitle,
|
customTitle,
|
||||||
guideMode)
|
guideMode,
|
||||||
|
preRollFiller,
|
||||||
|
midRollFiller,
|
||||||
|
postRollFiller,
|
||||||
|
tailFiller,
|
||||||
|
fallbackFiller)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using ErsatzTV.Application.Filler;
|
||||||
using ErsatzTV.Application.MediaCollections;
|
using ErsatzTV.Application.MediaCollections;
|
||||||
using ErsatzTV.Application.MediaItems;
|
using ErsatzTV.Application.MediaItems;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -20,7 +21,12 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
PlaybackOrder playbackOrder,
|
PlaybackOrder playbackOrder,
|
||||||
int count,
|
int count,
|
||||||
string customTitle,
|
string customTitle,
|
||||||
GuideMode guideMode) : base(
|
GuideMode guideMode,
|
||||||
|
FillerPresetViewModel preRollFiller,
|
||||||
|
FillerPresetViewModel midRollFiller,
|
||||||
|
FillerPresetViewModel postRollFiller,
|
||||||
|
FillerPresetViewModel tailFiller,
|
||||||
|
FillerPresetViewModel fallbackFiller) : base(
|
||||||
id,
|
id,
|
||||||
index,
|
index,
|
||||||
startType,
|
startType,
|
||||||
@@ -33,7 +39,12 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
mediaItem,
|
mediaItem,
|
||||||
playbackOrder,
|
playbackOrder,
|
||||||
customTitle,
|
customTitle,
|
||||||
guideMode) =>
|
guideMode,
|
||||||
|
preRollFiller,
|
||||||
|
midRollFiller,
|
||||||
|
postRollFiller,
|
||||||
|
tailFiller,
|
||||||
|
fallbackFiller) =>
|
||||||
Count = count;
|
Count = count;
|
||||||
|
|
||||||
public int Count { get; }
|
public int Count { get; }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using ErsatzTV.Application.Filler;
|
||||||
using ErsatzTV.Application.MediaCollections;
|
using ErsatzTV.Application.MediaCollections;
|
||||||
using ErsatzTV.Application.MediaItems;
|
using ErsatzTV.Application.MediaItems;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -19,7 +20,12 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
NamedMediaItemViewModel mediaItem,
|
NamedMediaItemViewModel mediaItem,
|
||||||
PlaybackOrder playbackOrder,
|
PlaybackOrder playbackOrder,
|
||||||
string customTitle,
|
string customTitle,
|
||||||
GuideMode guideMode) : base(
|
GuideMode guideMode,
|
||||||
|
FillerPresetViewModel preRollFiller,
|
||||||
|
FillerPresetViewModel midRollFiller,
|
||||||
|
FillerPresetViewModel postRollFiller,
|
||||||
|
FillerPresetViewModel tailFiller,
|
||||||
|
FillerPresetViewModel fallbackFiller) : base(
|
||||||
id,
|
id,
|
||||||
index,
|
index,
|
||||||
startType,
|
startType,
|
||||||
@@ -32,7 +38,12 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
mediaItem,
|
mediaItem,
|
||||||
playbackOrder,
|
playbackOrder,
|
||||||
customTitle,
|
customTitle,
|
||||||
guideMode)
|
guideMode,
|
||||||
|
preRollFiller,
|
||||||
|
midRollFiller,
|
||||||
|
postRollFiller,
|
||||||
|
tailFiller,
|
||||||
|
fallbackFiller)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using ErsatzTV.Application.Filler;
|
||||||
using ErsatzTV.Application.MediaCollections;
|
using ErsatzTV.Application.MediaCollections;
|
||||||
using ErsatzTV.Application.MediaItems;
|
using ErsatzTV.Application.MediaItems;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -18,7 +19,12 @@ namespace ErsatzTV.Application.ProgramSchedules
|
|||||||
NamedMediaItemViewModel MediaItem,
|
NamedMediaItemViewModel MediaItem,
|
||||||
PlaybackOrder PlaybackOrder,
|
PlaybackOrder PlaybackOrder,
|
||||||
string CustomTitle,
|
string CustomTitle,
|
||||||
GuideMode GuideMode)
|
GuideMode GuideMode,
|
||||||
|
FillerPresetViewModel PreRollFiller,
|
||||||
|
FillerPresetViewModel MidRollFiller,
|
||||||
|
FillerPresetViewModel PostRollFiller,
|
||||||
|
FillerPresetViewModel TailFiller,
|
||||||
|
FillerPresetViewModel FallbackFiller)
|
||||||
{
|
{
|
||||||
public string Name => CollectionType switch
|
public string Name => CollectionType switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
|
|||||||
.Include(i => i.MultiCollection)
|
.Include(i => i.MultiCollection)
|
||||||
.Include(i => i.SmartCollection)
|
.Include(i => i.SmartCollection)
|
||||||
.Include(i => i.MediaItem)
|
.Include(i => i.MediaItem)
|
||||||
.Include(i => (i as ProgramScheduleItemDuration).TailCollection)
|
|
||||||
.Include(i => (i as ProgramScheduleItemDuration).TailMultiCollection)
|
|
||||||
.Include(i => (i as ProgramScheduleItemDuration).TailSmartCollection)
|
|
||||||
.Include(i => (i as ProgramScheduleItemDuration).TailMediaItem)
|
|
||||||
.ThenInclude(i => (i as Movie).MovieMetadata)
|
.ThenInclude(i => (i as Movie).MovieMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
.Include(i => i.MediaItem)
|
.Include(i => i.MediaItem)
|
||||||
@@ -49,6 +45,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
|
|||||||
.Include(i => i.MediaItem)
|
.Include(i => i.MediaItem)
|
||||||
.ThenInclude(i => (i as Artist).ArtistMetadata)
|
.ThenInclude(i => (i as Artist).ArtistMetadata)
|
||||||
.ThenInclude(am => am.Artwork)
|
.ThenInclude(am => am.Artwork)
|
||||||
|
.Include(i => i.PreRollFiller)
|
||||||
|
.Include(i => i.MidRollFiller)
|
||||||
|
.Include(i => i.PostRollFiller)
|
||||||
|
.Include(i => i.TailFiller)
|
||||||
|
.Include(i => i.FallbackFiller)
|
||||||
.ToListAsync(cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
.Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel).ToList());
|
.Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel).ToList());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,18 +44,18 @@ namespace ErsatzTV.Application.Search.Commands
|
|||||||
await _configElementRepository.GetValue<int>(ConfigElementKey.SearchIndexVersion) <
|
await _configElementRepository.GetValue<int>(ConfigElementKey.SearchIndexVersion) <
|
||||||
_searchIndex.Version)
|
_searchIndex.Version)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Migrating search index to version {Version}", _searchIndex.Version);
|
_logger.LogInformation("Migrating search index to version {Version}", _searchIndex.Version);
|
||||||
|
|
||||||
List<int> itemIds = await _searchRepository.GetItemIdsToIndex();
|
List<int> itemIds = await _searchRepository.GetItemIdsToIndex();
|
||||||
await _searchIndex.Rebuild(_searchRepository, itemIds);
|
await _searchIndex.Rebuild(_searchRepository, itemIds);
|
||||||
|
|
||||||
await _configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, _searchIndex.Version);
|
await _configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, _searchIndex.Version);
|
||||||
|
|
||||||
_logger.LogDebug("Done migrating search index");
|
_logger.LogInformation("Done migrating search index");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Search index is already version {Version}", _searchIndex.Version);
|
_logger.LogInformation("Search index is already version {Version}", _searchIndex.Version);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ namespace ErsatzTV.Application.Search.Queries
|
|||||||
await GetIds(SearchIndex.EpisodeType, request.Query),
|
await GetIds(SearchIndex.EpisodeType, request.Query),
|
||||||
await GetIds(SearchIndex.ArtistType, request.Query),
|
await GetIds(SearchIndex.ArtistType, request.Query),
|
||||||
await GetIds(SearchIndex.MusicVideoType, request.Query),
|
await GetIds(SearchIndex.MusicVideoType, request.Query),
|
||||||
await GetIds(SearchIndex.OtherVideoType, request.Query));
|
await GetIds(SearchIndex.OtherVideoType, request.Query),
|
||||||
|
await GetIds(SearchIndex.SongType, request.Query));
|
||||||
|
|
||||||
private Task<List<int>> GetIds(string type, string query) =>
|
private Task<List<int>> GetIds(string type, string query) =>
|
||||||
_searchIndex.Search($"type:{type} AND ({query})", 0, 0)
|
_searchIndex.Search($"type:{type} AND ({query})", 0, 0)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Queries
|
||||||
|
{
|
||||||
|
public record QuerySearchIndexSongs
|
||||||
|
(string Query, int PageNumber, int PageSize) : IRequest<SongCardResultsViewModel>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Queries
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
QuerySearchIndexSongsHandler : IRequestHandler<QuerySearchIndexSongs,
|
||||||
|
SongCardResultsViewModel>
|
||||||
|
{
|
||||||
|
private readonly ISongRepository _songRepository;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
|
||||||
|
public QuerySearchIndexSongsHandler(ISearchIndex searchIndex, ISongRepository songRepository)
|
||||||
|
{
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
_songRepository = songRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SongCardResultsViewModel> Handle(
|
||||||
|
QuerySearchIndexSongs request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
SearchResult searchResult = await _searchIndex.Search(
|
||||||
|
request.Query,
|
||||||
|
(request.PageNumber - 1) * request.PageSize,
|
||||||
|
request.PageSize);
|
||||||
|
|
||||||
|
List<SongCardViewModel> items = await _songRepository
|
||||||
|
.GetSongsForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||||
|
|
||||||
|
return new SongCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,5 +9,6 @@ namespace ErsatzTV.Application.Search
|
|||||||
List<int> EpisodeIds,
|
List<int> EpisodeIds,
|
||||||
List<int> ArtistIds,
|
List<int> ArtistIds,
|
||||||
List<int> MusicVideoIds,
|
List<int> MusicVideoIds,
|
||||||
List<int> OtherVideoIds);
|
List<int> OtherVideoIds,
|
||||||
|
List<int> SongIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ namespace ErsatzTV.Application.Streaming.Commands
|
|||||||
private Task<Validation<BaseError, Unit>> SessionMustBeInactive(StartFFmpegSession request)
|
private Task<Validation<BaseError, Unit>> SessionMustBeInactive(StartFFmpegSession request)
|
||||||
{
|
{
|
||||||
var result = Optional(_ffmpegSegmenterService.SessionWorkers.TryAdd(request.ChannelNumber, null))
|
var result = Optional(_ffmpegSegmenterService.SessionWorkers.TryAdd(request.ChannelNumber, null))
|
||||||
.Filter(success => success)
|
.Where(success => success)
|
||||||
.Map(_ => Unit.Default)
|
.Map(_ => Unit.Default)
|
||||||
.ToValidation<BaseError>(new ChannelSessionAlreadyActive());
|
.ToValidation<BaseError>(new ChannelSessionAlreadyActive());
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user