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