Compare commits

..

122 Commits

Author SHA1 Message Date
Jellyfin Release Bot
a5b771861f Bump version to 10.10.6 2025-02-16 16:55:41 -05:00
gnattu
3f539472f3 Fix image encoding concurrency limit (#13532)
* Fix image encoding concurrency limit

The current FFmpeg image extractor is configured to use a resource pool size that always equals 2 times the number of CPU cores, which is somewhat excessive. Make the default equal to the core count instead of twice, and respect the `ParallelImageEncodingLimit` option.

* Fix code stype

* Check null value for unit tests
2025-02-12 18:45:36 -07:00
Cody Robibero
7f43521b64 Merge pull request #13489 from gnattu/fix-subnet-detect
Fix subnet contains check
2025-02-12 07:55:53 -07:00
gnattu
99006c370f Fix more typo 2025-02-10 05:20:34 +08:00
Cody Robibero
e3f9f0a7f3 Update dependency z440.atl.core to 6.16.0 (#13526) 2025-02-09 18:41:41 +01:00
elfalem
d1fbdcee34 Skip allowed tags check for parents of an item (#12721) 2025-02-09 09:49:02 -07:00
Tim Eisele
21e398ba0c Fix SchedulesDirect image prefetching (#13469)
* Only prefetch images for new and updated programs

* Update src/Jellyfin.LiveTv/Guide/GuideManager.cs
2025-02-09 09:45:03 -07:00
Marc Brooks
8544e7fc72 Fix LiveTV Guide Backdrop image not updating (#13504)
If the guide data changes an any other image, the Backdrop would not be updated.

Fixes #13496
2025-02-09 09:44:47 -07:00
Joshua M. Boniface
117d2082aa Merge pull request #13490 from gnattu/fix-audio-date-number-fallback
Correctly handle audio number tag fallbacks
2025-02-09 00:15:13 -05:00
Joshua M. Boniface
03082e90f9 Merge pull request #13499 from crobibero/api-key-websocket
Allow api key to subscribe to admin websockets
2025-02-09 00:14:46 -05:00
Cody Robibero
88026518b1 Allow api key to subscribe to admin websockets 2025-02-04 17:33:56 -07:00
Bond-009
5f1fb26382 Fix rating mistake in us.csv (#13487) 2025-02-04 17:10:31 -07:00
gnattu
070d04c1b2 Typo
Co-authored-by: Cody Robibero <cody@robibe.ro>
2025-02-05 08:04:43 +08:00
Cody Robibero
8aa4e2e320 Merge pull request #13439 from Bond-009/fallbackmimetype
Fall back to calculating mime type from path when needed
2025-02-04 17:00:55 -07:00
JPVenson
49bb5a6442 Merge pull request #13459 from JPVenson/bugfix/13457_FixWebSocketControllerConcurrency
Fixed Websocket not locking state correctly
2025-02-04 14:59:06 +01:00
Bond_009
9e869b4541 Simplify logic in ProviderManager.SaveImage 2025-02-04 14:56:52 +01:00
gnattu
710e877762 Correctly handle audio number tag fallbacks
Although the number type is nullable from the type definition of ATL, the lib might still normalize all unknown values to 0 which makes doing null check only not enough. Fallback to ffprobe results when the number is 0 as well.
2025-02-04 07:21:18 +08:00
gnattu
f536e08e14 Fix subnet contains check
We are still using `Subnet.Contains` a lot but that does not handle IPv4 mapped to IPv6 addresses at all. It was partially fixed by #12094 in local network checking, but it may not always happen on LAN.

Also make all local network checking to use IsInLocalNetwork method instead of just performing `Subnet.Contains` which is not accurate.

Filter out all link-local addresses for external interface matching.
2025-02-04 07:07:21 +08:00
Bond_009
4eecfee29f Fall back to calculating mime type from path when needed
Should fix #12593
2025-02-03 17:45:48 +01:00
Bond-009
731874429c Merge pull request #13448 from Shadowghost/fix-interface-selection-again
Fix interface ordering again
2025-02-03 17:43:11 +01:00
Tim Eisele
e6c6441abf Take subnet size into account 2025-01-27 10:59:50 +01:00
Tim Eisele
4d89a095ed Fix interface ordering again 2025-01-27 10:55:30 +01:00
Jellyfin Release Bot
1136a36eed Bump version to 10.10.5 2025-01-25 14:14:26 -05:00
gnattu
e8514de33b Don't select audio stream and codec explicitly for copy when bitrate exceeds limit (#13423) 2025-01-25 11:23:51 -07:00
JPVenson
722cdcce5e Add check to prevent downgrade from future EFCore refactor (#13103) 2025-01-25 10:11:19 -07:00
Bond-009
bfe0fdbcdc Open files with FileShare.Read for BlurHash calculations (#13425) 2025-01-25 08:41:51 -07:00
Joshua M. Boniface
0b2a59e963 Merge pull request #13384 from alltilla/fix-parallel-subtitleeditparse
Fix parallel use of not thread-safe SubtitleFormat instance
2025-01-25 02:32:00 -05:00
Joshua M. Boniface
6329de4fc3 Merge pull request #13411 from gnattu/use-writethrough-imagesaver
Use WriteThrough for ImageSaver
2025-01-25 02:29:39 -05:00
gnattu
644df3585b Use WriteThrough for ImageSaver
When writing an image to the disk, we use the completion of the async task as a signal indicating the completion of a write operation. However, this approach may not be entirely accurate, as the operating system can optimize IO operations by writing data to an intermediate cache instead of directly to the disk before completing the operation. This optimization can lead to a data race for our scanner, as subsequent tasks such as blurhash computation may attempt to read a file that has not yet been flushed from the volatile cache. Consequently, the data within the file becomes invalid, causing the blurhash computation task to fail.

Use WriteThrough mode to ensure the data is actual on disk before return to resolve this issue.
2025-01-24 07:54:22 +08:00
Joshua M. Boniface
3766a88bea Merge pull request #13390 from gnattu/catch-ioexception
Catch IOExceptions for GetFileSystemMetadata
2025-01-22 16:36:26 -05:00
Joshua M. Boniface
f333ef74b3 Merge pull request #13092 from TheMelmacian/bugfix/xml_special_characters_in_set_elements
Fix: handling of <set> elements in NfoParser
2025-01-22 16:36:04 -05:00
Joshua M. Boniface
0394965753 Merge pull request #13382 from Shadowghost/fix-published-url-override
Fix interface selection
2025-01-22 16:34:11 -05:00
Joshua M. Boniface
53a45c6033 Merge pull request #13388 from Shadowghost/fix-ratings
Fix rating levels
2025-01-22 16:34:02 -05:00
Shadowghost
adfe52f55a Fix rating levels 2025-01-22 21:32:43 +01:00
Jellyfin Release Bot
cf78aefbb7 Bump version to 10.10.4 2025-01-21 21:20:10 -05:00
Attila Szakacs
c693da94ce Fix parallel use of not thread-safe SubtitleFormat instance
`SubtitleFormat`'s `LoadSubtitle()` function is
not thread-safe.

A `SubtitleEditParser` instance's `Parse()`
function can be called from multiple threads at
the same time.

`SubtitleFormat`s are cached in the constructor
of each `SubtitleEditParser`, and the same
instances are used for each possibly parallel
`Parse()` function call, which causes subtitle
parse problems.

This patch modifies the code, so we only cache
the extension -> `SubtitleFormat` type/class
mapping and create a new `SubtitleFormat`
instance in each `Parse()` call, so no
`SubtitleFormat` instance is accessed from
multiple threads.

Fixes #12113

Kudos for everyone investigating the issue there,
most notably @RenV123 for PoC-ing the solution.

Signed-off-by: Attila Szakacs <szakacs.attila96@gmail.com>
2025-01-18 21:16:35 +01:00
gnattu
1a7c2299c6 Catch IOExceptions for GetFileSystemMetadata
Our `GetFileSystemEntries` method will throw when enumerating the file system, but its callers might consider the unhandled exceptions as the whole path is not available. This would cause a single problematic file to fail the enumeration, and could lead to unexpected side effects.

HandleIOException gracefully by marking the files throwing as not exist to let the caller skip that file.
2025-01-19 00:40:13 +08:00
Shadowghost
9c7d735a96 Fix interface selection 2025-01-17 09:41:32 +01:00
Bond-009
344cc8b97b Merge pull request #13345 from gnattu/fix-matroska-as-webm-audio
Never treat matroska as webm for audio playback
2025-01-14 15:00:31 +01:00
gnattu
cc9c000412 Never treat matroska as webm for audio playback
This would break browsers like Firefox where the matroska file cannot be played as audio file.
2025-01-10 15:24:10 +08:00
gnattu
5c6317f68d Use nv15 as intermediate format for 2-pass rkrga scaling (#13313) 2025-01-02 16:47:51 -07:00
gnattu
80940c0c57 Don't generate trickplay for backdrops (#13183) 2024-12-31 09:15:39 -07:00
gnattu
8aa41d5904 Transcode to audio codec satisfied other conditions when copy check failed. (#13209) 2024-12-31 09:15:05 -07:00
Tim Eisele
cea0c95942 Fix DTS in HLS (#13288) 2024-12-31 09:10:25 -07:00
Tim Eisele
4e28f4fe03 Fix missing episode removal (#13218) 2024-12-31 09:09:42 -07:00
Tim Eisele
f0e9b2fb96 Fix NFO ID parsing (#13167) 2024-12-31 09:06:45 -07:00
Tim Eisele
b9881b8bdf Fix EPG image caching (#13227) 2024-12-31 09:04:22 -07:00
Bond-009
b31f1696f2 Merge pull request #13151 from nyanmisaka/sw-tonemap-by-default
Always do tone-mapping for HDR transcoding when software pipeline is used
2024-12-29 22:29:46 +01:00
Bond-009
86160cd99c Merge pull request #13262 from gnattu/don't-use-x265-params-on-ultrafast
Don't use custom params on ultrafast x265 preset
2024-12-27 10:42:43 +01:00
Bond-009
230eacf15e Merge pull request #13280 from gnattu/backport-atl-update
Backport ATL update 6.11 to 10.10
2024-12-27 10:41:45 +01:00
gnattu
0ecaa98ee7 Backport ATL update 6.11 to 10.10
This fixed long duration (> 1hr) LRC formatting
2024-12-24 18:24:36 +08:00
gnattu
45c4bedbc6 Always apply necessary params 2024-12-21 22:09:56 +08:00
gnattu
2c4c1d054d Don't use custom params on ultrafast x265 preset
Our custom parameters are slower than the ultrafast preset, but users would expect encoding to be as fast as possible when selecting ultrafast. Only apply those parameters to superfast and slower presets.
2024-12-21 21:54:03 +08:00
Bond-009
f97f38585b Merge pull request #13182 from gnattu/no-multivalue-ffprobe-fallback
Don't fall back to ffprobe results for multi-value audio tags
2024-12-20 22:35:15 +01:00
Bond-009
a2a0cbf7ab Merge pull request #13180 from gnattu/backport-atl-update
Backport ATL update to 10.10
2024-12-09 22:05:00 +01:00
Bond-009
eb5f8d49dd Merge pull request #13187 from gnattu/properly-check-lan
Properly check LAN IP in HasRemoteAccess
2024-12-09 19:31:29 +01:00
Bond-009
6f7ce439d3 Merge pull request #13188 from Bond-009/nebml
Fix possible infinite loops in incomplete MKV files
2024-12-09 19:30:13 +01:00
Bond_009
03ea566271 Fix possible infinite loops in incomplete MKV files
https://github.com/OlegZee/NEbml/pull/14
Fixes #13122
2024-12-08 19:39:41 +01:00
gnattu
2a96b8b34b Properly check LAN IP in HasRemoteAccess
We cannot simply use the subnet list to check if the IP is in LAN as it does not handle special cases like IPv4MappedToIPv6 and IPv6 loopback addresses.
2024-12-08 22:06:11 +08:00
Bond-009
ff4f3b0441 Merge pull request #13169 from gnattu/fix-no-audio-transcoding
Check if the video has an audio track before codec fallback
2024-12-08 12:17:02 +01:00
gnattu
d49bb1d86d Don't fall back to ffprobe results for multi-value audio tags 2024-12-08 10:56:05 +08:00
renovate[bot]
cf6aa12627 Update dependency z440.atl.core to 6.9.0 2024-12-08 09:16:13 +08:00
gnattu
cd4519c15f Check if the video has an audio track before fallback
This would break transcoding for videos without an audio track as the codec checking would be null referencing.
2024-12-07 01:40:41 +08:00
nyanmisaka
8e248c7c05 Enable software tone-mapping by default
Transcoding HDR video without tonemapping results
in an unacceptable viewing experience. Many users
are not even aware of the option and therefore we
should always enable the software tonemapx filter.

Signed-off-by: nyanmisaka <nst7999610810@gmail.com>
2024-12-03 22:39:27 +08:00
gnattu
65f722f23c Fallback to lossy audio codec for bitrate limit (#13127) 2024-12-01 17:08:28 -07:00
TheMelmacian
5df03b9010 write Kodi conform set element to nfo files 2024-12-01 21:20:51 +01:00
gnattu
e7ac3e3929 Fix missing ConfigureAwait (#13139)
Regression from #12940
2024-12-01 10:57:37 -07:00
Bond-009
9464f9e622 Merge pull request #13113 from gnattu/only-remux-dv-when-no-fallback
Only do DoVi remux when the client supports profiles without fallbacks
2024-11-30 12:14:55 +01:00
Joshua M. Boniface
746280af0b Merge pull request #13106 from RealGreenDragon/patch-1
Enable RemoveOldPlugins by default (10.10.z backport)
2024-11-28 15:58:49 -05:00
gnattu
9bc6e8a306 Only do DoVi remux when the client supports profiles without fallbacks
In 10.10 clients that can only play the fallback layer like the Samsung TVs will report `DOVIWithHDR10` as supported video range, but the server should not do remux in DoVi as the client can only play the fallback layer. This changes the server to only do DoVi remux when the client can play DoVi videos without a fallback layer.
2024-11-26 15:01:59 +08:00
RealGreenDragon
b0105179eb Enable RemoveOldPlugins by default
Backport of PR #13102 to 10.10.z branch.
2024-11-25 08:40:20 +01:00
TheMelmacian
ef13a18450 fix(MovieNfoParser): parsing of <set> elements 2024-11-23 11:18:27 +01:00
Jellyfin Release Bot
b3e563385c Bump version to 10.10.3 2024-11-18 22:38:42 -05:00
Cody Robibero
5e45403cb1 Downgrade minimum sdk version (#13063) 2024-11-18 05:58:57 -07:00
Tim Eisele
23de7e517e Exclude file system based library playlists from migration (#13059) 2024-11-17 20:18:53 -07:00
Jellyfin Release Bot
be23f4eb0d Bump version to 10.10.2 2024-11-16 14:59:25 -05:00
Joshua M. Boniface
38c08c4fad Merge pull request #12916 from JPVenson/bugfix/10.10/MediaSegmentsRespectDisabledProviders
Added query filter to disregard disabled Providers
2024-11-16 14:25:56 -05:00
JPVenson
1b4ab5e777 pr review stuff 2024-11-16 18:39:11 +00:00
Akaanksh Raj
293e0f5faf Respect cancellation token/HTTP request aborts correctly in SymlinkFollowingPhysicalFileResultExecutor (#13033) 2024-11-16 10:16:43 -07:00
Cody Robibero
13ae2266de Merge pull request #13038 from Bond-009/stable-deps 2024-11-16 10:11:33 -07:00
renovate[bot]
6870e3496c Update dependency z440.atl.core to 6.8.0 2024-11-15 18:54:55 +01:00
renovate[bot]
ea88bdf2f3 Update dependency z440.atl.core to 6.7.0 (#12943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 18:54:52 +01:00
Bond-009
a6f04ffb7c Merge pull request #13021 from jellyfin/renovate/microsoft
Update Microsoft to 8.0.11
2024-11-15 18:52:49 +01:00
Bond-009
db266d75d6 Merge pull request #12986 from jellyfin/renovate/skiasharp-monorepo
Update skiasharp monorepo
2024-11-15 18:49:45 +01:00
Bond-009
f47d2c1f1a Merge pull request #12792 from jellyfin/renovate/dotnet-monorepo
Update dotnet monorepo
2024-11-15 18:49:14 +01:00
Tim Eisele
8bee67f1f8 Fix playlists (#12934) 2024-11-14 17:03:31 -07:00
Nyanmisaka
cf11a2dc1e Fix missing procamp vaapi filter (#13026) 2024-11-14 17:02:02 -07:00
gnattu
e2434d38c5 Only set first MusicBrainz ID for audio tags (#13003) 2024-11-14 17:01:48 -07:00
gnattu
9e61a6fd72 Always cleanup trickplay temp for ffmpeg failures (#13030) 2024-11-14 17:00:59 -07:00
gnattu
d292fde9e2 Use invariant culture for tonemap options (#12991) 2024-11-09 11:33:27 -07:00
Nyanmisaka
25321d7f80 Fix InvariantCulture in VPP tonemap options (#12989) 2024-11-09 11:31:59 -07:00
Joshua M. Boniface
9c6454ec46 Merge pull request #12955 from gnattu/fix-trickplay-regeneration
Fix trickplay images never being replaced
2024-11-09 10:19:32 -05:00
Joshua M. Boniface
09c377fb6c Merge pull request #12964 from nyanmisaka/fix-imported-trickplay-height
Fix height of imported trickplay tiles
2024-11-09 10:13:58 -05:00
gnattu
97dc02b163 Always consider null char as delimiter for ID3v2 (#12962) 2024-11-06 06:38:00 -07:00
Nyanmisaka
aa08d3f2bf Fix pixel format in HEVC RExt SDR transcoding (#12973) 2024-11-06 06:37:47 -07:00
nyanmisaka
2354cd45d4 Fix height of imported trickplay tiles
fixes c56dbc1

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2024-11-05 10:17:15 +08:00
gnattu
c8ca0c72e1 Fix trickplay images never being replaced
The Refresh API controller did not pass the query parameter from the client to MetadataRefreshOptions and the old trickplay files never got replaced.
2024-11-05 00:07:29 +08:00
gnattu
3089e9e40a Fix json array string writer in JsonDelimitedArrayConverter (#12949) 2024-11-04 08:04:04 -07:00
gnattu
954950dc14 Add a small tolerance value to remux fps check (#12947) 2024-11-04 07:59:23 -07:00
Jellyfin Release Bot
f6f4cdf9e7 Bump version to 10.10.1 2024-11-03 10:57:46 -05:00
Joshua M. Boniface
3a9b48a2aa Merge pull request #12940 from gnattu/remove-local-temp-file
Remove DynamicImageResponse local image after saved to metadata folder
2024-11-03 10:54:20 -05:00
gnattu
5769d5ca91 Catch all exceptions for file removal 2024-11-03 23:25:11 +08:00
gnattu
03271c43a7 Throw the exception as is 2024-11-03 16:10:17 +08:00
gnattu
bb30d26ffb Use ExceptionDispatchInfo 2024-11-03 04:28:48 +08:00
gnattu
e9ee0ef1f5 Remove temp file even when saving failed 2024-11-03 04:11:41 +08:00
gnattu
3aefbf8cf6 Don't do double remove in BaseDynamicImageProvider 2024-11-03 03:02:35 +08:00
gnattu
469bf9d514 Move the remove source implementation into ProviderManager 2024-11-03 02:51:11 +08:00
Niels van Velzen
a165883999 Merge pull request #12931 from gnattu/set-audio-codec-when-transcoding
Set AudioCodec when building stream
2024-11-02 19:11:34 +01:00
gnattu
74d2c2addf Remove DynamicImageResponse local image after saved to metadata folder
Previously, local images provided by DynamicImageResponse were never cleaned up until the server was restarted. This issue has become more severe in 10.10, as the default is now set to use the system's native temp folder, which might be a RAM backed tmpfs. This behavior could lead to resource starvation for long-running servers performing multiple library scans.

Metadata plugins prefer the old behavior should do its own backup.
2024-11-02 17:15:00 +08:00
gnattu
096e1b2970 Add comments noting that comma separated codec list is not supported in pure audio transcoding for now 2024-11-01 07:09:16 +08:00
gnattu
b0f44f1d5a Set AudioCodec when building stream
This was not set at least since 10.9 and the transcoding behavior is close to "undefined" and in 10.10 this will not work at all. This will make the returned transcoding url from PlayBackInfo to correctly specify the desired transcoding codec. If the client wants to use the HLS controller directly it should be responsible to provide valid container and codec in the parameters.
2024-11-01 05:49:31 +08:00
JPVenson
584be05e93 reduced providerid build 2024-10-31 17:51:56 +00:00
JPVenson
3592c629e7 Fixed possible NullReferenceException in SessionManager (#12915) 2024-10-31 09:40:48 -06:00
Mikal S.
f99e0407fd Don't try to prune images for virtual episodes. (#12909) 2024-10-31 09:40:03 -06:00
JPVenson
fe9c6fb8ae Fixed enumerable 2024-10-31 07:40:47 +00:00
JPVenson
54a6a33c01 renamed param 2024-10-30 10:31:10 +00:00
JPVenson
0130580151 Fixed interface definition 2024-10-30 10:25:57 +00:00
JPVenson
aa4dd04b99 Added fast fail for no provider selected segment query 2024-10-30 10:10:55 +00:00
JPVenson
c08d1d5b7f Added parameter to enable or disable library filter 2024-10-30 10:09:39 +00:00
JPVenson
312ff4f3d8 Fixed disabled providers not beeing returned 2024-10-30 10:05:52 +00:00
Benedikt
c6629aebf8 Fix TMDB import failing when no IMDB ID is set for a movie (#12891) 2024-10-28 07:29:15 -06:00
Jellyfin Release Bot
016a7e5542 Bump version to 10.10.0 2024-10-26 13:32:50 -04:00
1188 changed files with 21899 additions and 76314 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "9.0.11", "version": "8.0.11",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ]

View File

@@ -0,0 +1,28 @@
{
"name": "Development Jellyfin Server - FFmpeg",
"image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
"postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"",
// reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
"dotnetRuntimeVersions": "8.0",
"aspNetCoreRuntimeVersions": "8.0"
},
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
"preserve_apt_list": false,
"packages": ["libfontconfig1"]
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"dockerDashComposeVersion": "v2"
},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
},
"hostRequirements": {
"memory": "8gb",
"cpus": 4
}
}

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
## configure the following for a manual install of a specific version from the repo ## configure the following for a manuall install of a specific version from the repo
# wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb # wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb
@@ -29,4 +29,4 @@ Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF EOF
sudo apt update -y sudo apt update -y
sudo apt install jellyfin-ffmpeg7 -y sudo apt install jellyfin-ffmpeg6 -y

View File

@@ -1,23 +1,19 @@
{ {
"name": "Development Jellyfin Server", "name": "Development Jellyfin Server",
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", "image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate // restores nuget packages, installs the dotnet workloads and installs the dev https certificate
"postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust",
// reads the extensions list and installs them // reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension", "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": { "features": {
"ghcr.io/devcontainers/features/dotnet:2": { "ghcr.io/devcontainers/features/dotnet:2": {
"version": "none", "version": "none",
"dotnetRuntimeVersions": "9.0", "dotnetRuntimeVersions": "8.0",
"aspNetCoreRuntimeVersions": "9.0" "aspNetCoreRuntimeVersions": "8.0"
}, },
"ghcr.io/devcontainers-extra/features/apt-packages:1": { "ghcr.io/devcontainers-contrib/features/apt-packages:1": {
"preserve_apt_list": false, "preserve_apt_list": false,
"packages": [ "packages": ["libfontconfig1"]
"libfontconfig1"
]
}, },
"ghcr.io/devcontainers/features/docker-in-docker:2": { "ghcr.io/devcontainers/features/docker-in-docker:2": {
"dockerDashComposeVersion": "v2" "dockerDashComposeVersion": "v2"

View File

@@ -192,344 +192,3 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences # Wrapping preferences
csharp_preserve_single_line_statements = true csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true csharp_preserve_single_line_blocks = true
###############################
# C# Analyzer Rules #
###############################
### ERROR #
###########
# error on SA1000: The keyword 'new' should be followed by a space
dotnet_diagnostic.SA1000.severity = error
# error on SA1001: Commas should not be preceded by whitespace
dotnet_diagnostic.SA1001.severity = error
# error on SA1106: Code should not contain empty statements
dotnet_diagnostic.SA1106.severity = error
# error on SA1107: Code should not contain multiple statements on one line
dotnet_diagnostic.SA1107.severity = error
# error on SA1028: Code should not contain trailing whitespace
dotnet_diagnostic.SA1028.severity = error
# error on SA1117: The parameters should all be placed on the same line or each parameter should be placed on its own line
dotnet_diagnostic.SA1117.severity = error
# error on SA1137: Elements should have the same indentation
dotnet_diagnostic.SA1137.severity = error
# error on SA1142: Refer to tuple fields by name
dotnet_diagnostic.SA1142.severity = error
# error on SA1210: Using directives should be ordered alphabetically by the namespaces
dotnet_diagnostic.SA1210.severity = error
# error on SA1316: Tuple element names should use correct casing
dotnet_diagnostic.SA1316.severity = error
# error on SA1414: Tuple types in signatures should have element names
dotnet_diagnostic.SA1414.severity = error
# disable warning SA1513: Closing brace should be followed by blank line
dotnet_diagnostic.SA1513.severity = error
# error on SA1518: File is required to end with a single newline character
dotnet_diagnostic.SA1518.severity = error
# error on SA1629: Documentation text should end with a period
dotnet_diagnostic.SA1629.severity = error
# error on CA1001: Types that own disposable fields should be disposable
dotnet_diagnostic.CA1001.severity = error
# error on CA1012: Abstract types should not have public constructors
dotnet_diagnostic.CA1012.severity = error
# error on CA1063: Implement IDisposable correctly
dotnet_diagnostic.CA1063.severity = error
# error on CA1305: Specify IFormatProvider
dotnet_diagnostic.CA1305.severity = error
# error on CA1307: Specify StringComparison for clarity
dotnet_diagnostic.CA1307.severity = error
# error on CA1309: Use ordinal StringComparison
dotnet_diagnostic.CA1309.severity = error
# error on CA1310: Specify StringComparison for correctness
dotnet_diagnostic.CA1310.severity = error
# error on CA1513: Use 'ObjectDisposedException.ThrowIf' instead of explicitly throwing a new exception instance
dotnet_diagnostic.CA1513.severity = error
# error on CA1725: Parameter names should match base declaration
dotnet_diagnostic.CA1725.severity = error
# error on CA1725: Call async methods when in an async method
dotnet_diagnostic.CA1727.severity = error
# error on CA1813: Avoid unsealed attributes
dotnet_diagnostic.CA1813.severity = error
# error on CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string
dotnet_diagnostic.CA1834.severity = error
# error on CA1843: Do not use 'WaitAll' with a single task
dotnet_diagnostic.CA1843.severity = error
# error on CA1845: Use span-based 'string.Concat'
dotnet_diagnostic.CA1845.severity = error
# error on CA1849: Call async methods when in an async method
dotnet_diagnostic.CA1849.severity = error
# error on CA1851: Possible multiple enumerations of IEnumerable collection
dotnet_diagnostic.CA1851.severity = error
# error on CA1854: Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup
dotnet_diagnostic.CA1854.severity = error
# error on CA1860: Avoid using 'Enumerable.Any()' extension method
dotnet_diagnostic.CA1860.severity = error
# error on CA1861: Avoid constant arrays as arguments
dotnet_diagnostic.CA1861.severity = error
# error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
dotnet_diagnostic.CA1862.severity = error
# error on CA1863: Use 'CompositeFormat'
dotnet_diagnostic.CA1863.severity = error
# error on CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method
dotnet_diagnostic.CA1864.severity = error
# error on CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
dotnet_diagnostic.CA1865.severity = error
dotnet_diagnostic.CA1866.severity = error
dotnet_diagnostic.CA1867.severity = error
# error on CA1868: Unnecessary call to 'Contains' for sets
dotnet_diagnostic.CA1868.severity = error
# error on CA1869: Cache and reuse 'JsonSerializerOptions' instances
dotnet_diagnostic.CA1869.severity = error
# error on CA1870: Use a cached 'SearchValues' instance
dotnet_diagnostic.CA1870.severity = error
# error on CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'
dotnet_diagnostic.CA1871.severity = error
# error on CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'
dotnet_diagnostic.CA1872.severity = error
# error on CA2016: Forward the CancellationToken parameter to methods that take one
# or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token
dotnet_diagnostic.CA2016.severity = error
# error on CA2201: Exception type System.Exception is not sufficiently specific
dotnet_diagnostic.CA2201.severity = error
# error on CA2215: Dispose methods should call base class dispose
dotnet_diagnostic.CA2215.severity = error
# error on CA2249: Use 'string.Contains' instead of 'string.IndexOf' to improve readability
dotnet_diagnostic.CA2249.severity = error
# error on CA2254: Template should be a static expression
dotnet_diagnostic.CA2254.severity = error
################
### SUGGESTION #
################
# disable warning CA1014: Mark assemblies with CLSCompliantAttribute
dotnet_diagnostic.CA1014.severity = suggestion
# disable warning CA1024: Use properties where appropriate
dotnet_diagnostic.CA1024.severity = suggestion
# disable warning CA1031: Do not catch general exception types
dotnet_diagnostic.CA1031.severity = suggestion
# disable warning CA1032: Implement standard exception constructors
dotnet_diagnostic.CA1032.severity = suggestion
# disable warning CA1040: Avoid empty interfaces
dotnet_diagnostic.CA1040.severity = suggestion
# disable warning CA1062: Validate arguments of public methods
dotnet_diagnostic.CA1062.severity = suggestion
# TODO: enable when false positives are fixed
# disable warning CA1508: Avoid dead conditional code
dotnet_diagnostic.CA1508.severity = suggestion
# disable warning CA1515: Consider making public types internal
dotnet_diagnostic.CA1515.severity = suggestion
# disable warning CA1716: Identifiers should not match keywords
dotnet_diagnostic.CA1716.severity = suggestion
# disable warning CA1720: Identifiers should not contain type names
dotnet_diagnostic.CA1720.severity = suggestion
# disable warning CA1724: Type names should not match namespaces
dotnet_diagnostic.CA1724.severity = suggestion
# disable warning CA1805: Do not initialize unnecessarily
dotnet_diagnostic.CA1805.severity = suggestion
# disable warning CA1812: internal class that is apparently never instantiated.
# If so, remove the code from the assembly.
# If this class is intended to contain only static members, make it static
dotnet_diagnostic.CA1812.severity = suggestion
# disable warning CA1822: Member does not access instance data and can be marked as static
dotnet_diagnostic.CA1822.severity = suggestion
# CA1859: Use concrete types when possible for improved performance
dotnet_diagnostic.CA1859.severity = suggestion
# TODO: Enable
# CA1861: Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array
dotnet_diagnostic.CA1861.severity = suggestion
# disable warning CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = suggestion
# disable warning CA2253: Named placeholders should not be numeric values
dotnet_diagnostic.CA2253.severity = suggestion
# disable warning CA5394: Do not use insecure randomness
dotnet_diagnostic.CA5394.severity = suggestion
# error on CA3003: Review code for file path injection vulnerabilities
dotnet_diagnostic.CA3003.severity = suggestion
# error on CA3006: Review code for process command injection vulnerabilities
dotnet_diagnostic.CA3006.severity = suggestion
###############
### DISABLED #
###############
# disable warning SA1009: Closing parenthesis should be followed by a space.
dotnet_diagnostic.SA1009.severity = none
# disable warning SA1011: Closing square bracket should be followed by a space.
dotnet_diagnostic.SA1011.severity = none
# disable warning SA1101: Prefix local calls with 'this.'
dotnet_diagnostic.SA1101.severity = none
# disable warning SA1108: Block statements should not contain embedded comments
dotnet_diagnostic.SA1108.severity = none
# disable warning SA1118: Parameter must not span multiple lines.
dotnet_diagnostic.SA1118.severity = none
# disable warning SA1128:: Put constructor initializers on their own line
dotnet_diagnostic.SA1128.severity = none
# disable warning SA1130: Use lambda syntax
dotnet_diagnostic.SA1130.severity = none
# disable warning SA1200: 'using' directive must appear within a namespace declaration
dotnet_diagnostic.SA1200.severity = none
# disable warning SA1202: 'public' members must come before 'private' members
dotnet_diagnostic.SA1202.severity = none
# disable warning SA1204: Static members must appear before non-static members
dotnet_diagnostic.SA1204.severity = none
# disable warning SA1309: Fields must not begin with an underscore
dotnet_diagnostic.SA1309.severity = none
# disable warning SA1311: Static readonly fields should begin with upper-case letter
dotnet_diagnostic.SA1311.severity = none
# disable warning SA1413: Use trailing comma in multi-line initializers
dotnet_diagnostic.SA1413.severity = none
# disable warning SA1512: Single-line comments must not be followed by blank line
dotnet_diagnostic.SA1512.severity = none
# disable warning SA1515: Single-line comment should be preceded by blank line
dotnet_diagnostic.SA1515.severity = none
# disable warning SA1600: Elements should be documented
dotnet_diagnostic.SA1600.severity = none
# disable warning SA1601: Partial elements should be documented
dotnet_diagnostic.SA1601.severity = none
# disable warning SA1602: Enumeration items should be documented
dotnet_diagnostic.SA1602.severity = none
# disable warning SA1633: The file header is missing or not located at the top of the file
dotnet_diagnostic.SA1633.severity = none
# disable warning CA1054: Change the type of parameter url from string to System.Uri
dotnet_diagnostic.CA1054.severity = none
# disable warning CA1055: URI return values should not be strings
dotnet_diagnostic.CA1055.severity = none
# disable warning CA1056: URI properties should not be strings
dotnet_diagnostic.CA1056.severity = none
# disable warning CA1303: Do not pass literals as localized parameters
dotnet_diagnostic.CA1303.severity = none
# disable warning CA1308: Normalize strings to uppercase
dotnet_diagnostic.CA1308.severity = none
# disable warning CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.CA1848.severity = none
# disable warning CA2101: Specify marshaling for P/Invoke string arguments
dotnet_diagnostic.CA2101.severity = none
# disable warning CA2234: Pass System.Uri objects instead of strings
dotnet_diagnostic.CA2234.severity = none
# error on RS0030: Do not used banned APIs
dotnet_diagnostic.RS0030.severity = error
# disable warning IDISP001: Dispose created
dotnet_diagnostic.IDISP001.severity = suggestion
# TODO: Enable when false positives are fixed
# disable warning IDISP003: Dispose previous before re-assigning
dotnet_diagnostic.IDISP003.severity = suggestion
# disable warning IDISP004: Don't ignore created IDisposable
dotnet_diagnostic.IDISP004.severity = suggestion
# disable warning IDISP007: Don't dispose injected
dotnet_diagnostic.IDISP007.severity = suggestion
# disable warning IDISP008: Don't assign member with injected and created disposables
dotnet_diagnostic.IDISP008.severity = suggestion
[tests/**.{cs,vb}]
# disable warning SA0001: XML comment analysis is disabled due to project configuration
dotnet_diagnostic.SA0001.severity = none
# disable warning CA1707: Identifiers should not contain underscores
dotnet_diagnostic.CA1707.severity = none
# disable warning CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = none
# disable warning CA2234: Pass system uri objects instead of strings
dotnet_diagnostic.CA2234.severity = suggestion
# disable warning xUnit1028: Test methods must have a supported return type.
dotnet_diagnostic.xUnit1028.severity = none
# CA1826: Do not use Enumerable methods on indexable collections
dotnet_diagnostic.CA1826.severity = suggestion

15
.github/CODEOWNERS vendored
View File

@@ -1,11 +1,4 @@
# Joshua must review all changes to bump_version and any files it touches # Joshua must review all changes to deployment and build.sh
bump_version @joshuaboniface .ci/* @joshuaboniface
.github/ISSUE_TEMPLATE @joshuaboniface deployment/* @joshuaboniface
MediaBrowser.Common/MediaBrowser.Common.csproj @joshuaboniface build.sh @joshuaboniface
Jellyfin.Data/Jellyfin.Data.csproj @joshuaboniface
MediaBrowser.Controller/MediaBrowser.Controller.csproj @joshuaboniface
MediaBrowser.Model/MediaBrowser.Model.csproj @joshuaboniface
Emby.Naming/Emby.Naming.csproj @joshuaboniface
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @joshuaboniface
# Core must approve all changes within the repo config
.github/ @jellyfin/core

View File

@@ -1,7 +1,6 @@
name: Issue Report name: Issue Report
description: File an issue report description: File an issue report
labels: [bug, triage] labels: [bug, triage]
type: Bug
body: body:
- type: markdown - type: markdown
id: introduction id: introduction
@@ -15,7 +14,7 @@ body:
label: "This issue respects the following points:" label: "This issue respects the following points:"
description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment. description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment.
options: options:
- label: This is a **bug**, not a question or a configuration issue; Please visit our [forum or chat rooms](https://jellyfin.org/contact/) first to troubleshoot with volunteers, before creating a report. - label: This is a **bug**, not a question or a configuration issue; Please visit our forum or chat rooms first to troubleshoot with volunteers, before creating a report. The links can be found [here](https://jellyfin.org/contact/).
required: true required: true
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_. - label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_.
required: true required: true
@@ -87,10 +86,7 @@ body:
label: Jellyfin Server version label: Jellyfin Server version
description: What version of Jellyfin are you using? description: What version of Jellyfin are you using?
options: options:
- 10.11.3 - 10.9.11+
- 10.11.2
- 10.11.1
- 10.11.0
- Master - Master
- Unstable - Unstable
- Older* - Older*
@@ -144,9 +140,7 @@ body:
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.] - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example] - **Base URL**: [e.g. none, yes: /example]
- **Networking**: [e.g. Host, Bridge/NAT] - **Networking**: [e.g. Host, Bridge/NAT]
- **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD] - **Storage**: [e.g. local, NFS, cloud]
- **Media Storage**: [e.g. Local HDD, SMB Share]
- **External Integrations**: [e.g. Jellystat, Jellyseerr]
value: | value: |
- OS: - OS:
- Linux Kernel: - Linux Kernel:
@@ -161,9 +155,7 @@ body:
- Reverse Proxy: - Reverse Proxy:
- Base URL: - Base URL:
- Networking: - Networking:
- Jellyfin Data Storage: - Storage:
- Media Storage:
- External Integrations:
render: markdown render: markdown
validations: validations:
required: true required: true

View File

@@ -20,18 +20,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
with: with:
dotnet-version: '9.0.x' dotnet-version: '8.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0

View File

@@ -11,22 +11,17 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with:
dotnet-version: '9.0.x'
- name: Build - name: Build
run: | run: |
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: abi-head name: abi-head
retention-days: 14 retention-days: 14
@@ -40,17 +35,12 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0 fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
with:
dotnet-version: '9.0.x'
- name: Checkout common ancestor - name: Checkout common ancestor
env: env:
HEAD_REF: ${{ github.head_ref }} HEAD_REF: ${{ github.head_ref }}
@@ -65,7 +55,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: abi-base name: abi-base
retention-days: 14 retention-days: 14
@@ -85,13 +75,13 @@ jobs:
steps: steps:
- name: Download abi-head - name: Download abi-head
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: with:
name: abi-head name: abi-head
path: abi-head path: abi-head
- name: Download abi-base - name: Download abi-base
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: with:
name: abi-base name: abi-base
path: abi-base path: abi-base
@@ -105,7 +95,7 @@ jobs:
run: | run: |
{ {
echo 'body<<EOF' echo 'body<<EOF'
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll; do
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )" COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
printf "\n${file}\n${COMPAT_OUTPUT}\n" printf "\n${file}\n${COMPAT_OUTPUT}\n"
@@ -115,7 +105,7 @@ jobs:
} >> $GITHUB_OUTPUT } >> $GITHUB_OUTPUT
- name: Find difference comment - name: Find difference comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment id: find-comment
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@@ -123,7 +113,7 @@ jobs:
body-includes: abi-diff-workflow-comment body-includes: abi-diff-workflow-comment
- name: Reply or edit difference comment (changed) - name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.diff.outputs.body != '' }} if: ${{ steps.diff.outputs.body != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@@ -142,7 +132,7 @@ jobs:
</details> </details>
- name: Reply or edit difference comment (unchanged) - name: Reply or edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}

View File

@@ -16,23 +16,23 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
with: with:
dotnet-version: '9.0.x' dotnet-version: '8.0.x'
- name: Generate openapi.json - name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json - name: Upload openapi.json
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: openapi-head name: openapi-head
retention-days: 14 retention-days: 14
if-no-files-found: error if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
openapi-base: openapi-base:
name: OpenAPI - BASE name: OpenAPI - BASE
@@ -41,7 +41,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -55,18 +55,18 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF) ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
with: with:
dotnet-version: '9.0.x' dotnet-version: '8.0.x'
- name: Generate openapi.json - name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json - name: Upload openapi.json
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: openapi-base name: openapi-base
retention-days: 14 retention-days: 14
if-no-files-found: error if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
openapi-diff: openapi-diff:
permissions: permissions:
@@ -80,12 +80,12 @@ jobs:
- openapi-base - openapi-base
steps: steps:
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
- name: Download openapi-base - name: Download openapi-base
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: with:
name: openapi-base name: openapi-base
path: openapi-base path: openapi-base
@@ -120,14 +120,14 @@ jobs:
echo "" >> openapi-changes-reply.md echo "" >> openapi-changes-reply.md
echo "</details>" >> openapi-changes-reply.md echo "</details>" >> openapi-changes-reply.md
- name: Find difference comment - name: Find difference comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment id: find-comment
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
direction: last direction: last
body-includes: openapi-diff-workflow-comment body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed) - name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }} if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@@ -135,7 +135,7 @@ jobs:
edit-mode: replace edit-mode: replace
body-path: openapi-changes-reply.md body-path: openapi-changes-reply.md
- name: Edit difference comment (unchanged) - name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }} if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@@ -158,12 +158,12 @@ jobs:
run: |- run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
- name: Upload openapi.json (unstable) to repository server - name: Upload openapi.json (unstable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
with: with:
host: "${{ secrets.REPO_HOST }}" host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"
@@ -172,7 +172,7 @@ jobs:
strip_components: 1 strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place - name: Move openapi.json (unstable) into place
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4 uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
with: with:
host: "${{ secrets.REPO_HOST }}" host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"
@@ -220,12 +220,12 @@ jobs:
run: |- run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
- name: Upload openapi.json (stable) to repository server - name: Upload openapi.json (stable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
with: with:
host: "${{ secrets.REPO_HOST }}" host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"
@@ -234,7 +234,7 @@ jobs:
strip_components: 1 strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place - name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4 uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
with: with:
host: "${{ secrets.REPO_HOST }}" host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"

View File

@@ -9,20 +9,19 @@ on:
pull_request: pull_request:
env: env:
SDK_VERSION: "9.0.x" SDK_VERSION: "8.0.x"
jobs: jobs:
run-tests: run-tests:
strategy: strategy:
matrix: matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"] os: ["ubuntu-latest", "macos-latest", "windows-latest"]
fail-fast: false
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
steps: steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
with: with:
dotnet-version: ${{ env.SDK_VERSION }} dotnet-version: ${{ env.SDK_VERSION }}
@@ -35,7 +34,7 @@ jobs:
--verbosity minimal --verbosity minimal
- name: Merge code coverage results - name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@dcdfb6e704e87df6b2ed0cf123a6c9f69e364869 # v5.5.0 uses: danielpalme/ReportGenerator-GitHub-Action@62f9e70ab348d56eee76d446b4db903a85ab0ea8 # v5.3.11
with: with:
reports: "**/coverage.cobertura.xml" reports: "**/coverage.cobertura.xml"
targetdir: "merged/" targetdir: "merged/"

View File

@@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - name: Notify as seen
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }} comment-id: ${{ github.event.comment.id }}
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@@ -34,19 +34,107 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
check-backport:
permissions:
contents: read
name: Check Backport
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: eyes
- name: Checkout the latest code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Running backport tests...
- name: Perform test backport
id: run_tests
run: |
set +o errexit
git config --global user.name "Jellyfin Bot"
git config --global user.email "team@jellyfin.org"
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
git checkout master
git merge --no-ff ${CURRENT_BRANCH}
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
git fetch --all
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
echo ${stable_branch}
echo ::set-output name=branch::${stable_branch}
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
retcode=$?
cat output.txt | grep -v 'hint:'
output="$( grep -v 'hint:' output.txt )"
output="${output//'%'/'%25'}"
output="${output//$'\n'/'%0A'}"
output="${output//$'\r'/'%0D'}"
echo ::set-output name=output::$output
exit ${retcode}
- name: Notify with result success
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ steps.comment_running.outputs.comment-id }}
body: |
${{ steps.run_tests.outputs.branch }}
Output from `git cherry-pick`:
---
${{ steps.run_tests.outputs.output }}
reactions: hooray
- name: Notify with result failure
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ steps.comment_running.outputs.comment-id }}
body: |
${{ steps.run_tests.outputs.branch }}
Output from `git cherry-pick`:
---
${{ steps.run_tests.outputs.output }}
reactions: confused
rename: rename:
name: Rename name: Rename
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER' if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: pull in script - name: pull in script
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
repository: jellyfin/jellyfin-triage-script repository: jellyfin/jellyfin-triage-script
- name: install python - name: install python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with: with:
python-version: '3.14' python-version: '3.12'
cache: 'pip' cache: 'pip'
- name: install python packages - name: install python packages
run: pip install -r rename/requirements.txt run: pip install -r rename/requirements.txt

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true ascending: true

View File

@@ -10,13 +10,13 @@ jobs:
issues: write issues: write
steps: steps:
- name: pull in script - name: pull in script
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
repository: jellyfin/jellyfin-triage-script repository: jellyfin/jellyfin-triage-script
- name: install python - name: install python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with: with:
python-version: '3.14' python-version: '3.12'
cache: 'pip' cache: 'pip'
- name: install python packages - name: install python packages
run: pip install -r main-repo-triage/requirements.txt run: pip install -r main-repo-triage/requirements.txt

View File

@@ -12,10 +12,10 @@ jobs:
label: label:
name: Labeling name: Labeling
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' && github.event.issue.pull_request }} if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps: steps:
- name: Apply label - name: Apply label
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with: with:
dirtyLabel: 'merge conflict' dirtyLabel: 'merge conflict'

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true ascending: true

View File

@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8 yq-version: v4.9.8
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}

View File

@@ -1,13 +1,12 @@
{ {
"recommendations": [ "recommendations": [
"ms-dotnettools.csharp", "ms-dotnettools.csharp",
"editorconfig.editorconfig", "editorconfig.editorconfig",
"github.vscode-github-actions", "github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime", "ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.csdevkit", "ms-dotnettools.csdevkit"
"alexcvzz.vscode-sqlite" ],
], "unwantedRecommendations": [
"unwantedRecommendations": [
] ]
} }

6
.vscode/launch.json vendored
View File

@@ -6,7 +6,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
"args": [], "args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
"args": ["--nowebclient"], "args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",
@@ -34,7 +34,7 @@
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll", "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"], "args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
"cwd": "${workspaceFolder}/Jellyfin.Server", "cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole", "console": "internalConsole",

View File

@@ -1,3 +0,0 @@
{
"dotnet.preferVisualStudioCodeFileSystemWatcher": true
}

View File

@@ -27,11 +27,9 @@
- [cryptobank](https://github.com/cryptobank) - [cryptobank](https://github.com/cryptobank)
- [cvium](https://github.com/cvium) - [cvium](https://github.com/cvium)
- [dannymichel](https://github.com/dannymichel) - [dannymichel](https://github.com/dannymichel)
- [darioackermann](https://github.com/darioackermann)
- [DaveChild](https://github.com/DaveChild) - [DaveChild](https://github.com/DaveChild)
- [DavidFair](https://github.com/DavidFair) - [DavidFair](https://github.com/DavidFair)
- [Delgan](https://github.com/Delgan) - [Delgan](https://github.com/Delgan)
- [Derpipose](https://github.com/Derpipose)
- [dcrdev](https://github.com/dcrdev) - [dcrdev](https://github.com/dcrdev)
- [dhartung](https://github.com/dhartung) - [dhartung](https://github.com/dhartung)
- [dinki](https://github.com/dinki) - [dinki](https://github.com/dinki)
@@ -62,7 +60,6 @@
- [ikomhoog](https://github.com/ikomhoog) - [ikomhoog](https://github.com/ikomhoog)
- [iwalton3](https://github.com/iwalton3) - [iwalton3](https://github.com/iwalton3)
- [jftuga](https://github.com/jftuga) - [jftuga](https://github.com/jftuga)
- [jkhsjdhjs](https://github.com/jkhsjdhjs)
- [jmshrv](https://github.com/jmshrv) - [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h) - [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface) - [joshuaboniface](https://github.com/joshuaboniface)
@@ -141,7 +138,6 @@
- [ThibaultNocchi](https://github.com/ThibaultNocchi) - [ThibaultNocchi](https://github.com/ThibaultNocchi)
- [thornbill](https://github.com/thornbill) - [thornbill](https://github.com/thornbill)
- [ThreeFive-O](https://github.com/ThreeFive-O) - [ThreeFive-O](https://github.com/ThreeFive-O)
- [tjwalkr3](https://github.com/tjwalkr3)
- [TrisMcC](https://github.com/TrisMcC) - [TrisMcC](https://github.com/TrisMcC)
- [trumblejoe](https://github.com/trumblejoe) - [trumblejoe](https://github.com/trumblejoe)
- [TtheCreator](https://github.com/TtheCreator) - [TtheCreator](https://github.com/TtheCreator)
@@ -196,15 +192,8 @@
- [jaina heartles](https://github.com/heartles) - [jaina heartles](https://github.com/heartles)
- [oxixes](https://github.com/oxixes) - [oxixes](https://github.com/oxixes)
- [elfalem](https://github.com/elfalem) - [elfalem](https://github.com/elfalem)
- [Kenneth Cochran](https://github.com/kennethcochran)
- [benedikt257](https://github.com/benedikt257) - [benedikt257](https://github.com/benedikt257)
- [revam](https://github.com/revam) - [revam](https://github.com/revam)
- [allesmi](https://github.com/allesmi)
- [ThunderClapLP](https://github.com/ThunderClapLP)
- [Shoham Peller](https://github.com/spellr)
- [theshoeshiner](https://github.com/theshoeshiner)
- [TokerX](https://github.com/TokerX)
- [GeneMarks](https://github.com/GeneMarks)
# Emby Contributors # Emby Contributors
@@ -278,6 +267,3 @@
- [0x25CBFC4F](https://github.com/0x25CBFC4F) - [0x25CBFC4F](https://github.com/0x25CBFC4F)
- [Robert Lützner](https://github.com/rluetzner) - [Robert Lützner](https://github.com/rluetzner)
- [Nathan McCrina](https://github.com/nfmccrina) - [Nathan McCrina](https://github.com/nfmccrina)
- [Martin Reuter](https://github.com/reuterma24)
- [Michael McElroy](https://github.com/mcmcelro)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)

View File

@@ -3,11 +3,11 @@
<PropertyGroup> <PropertyGroup>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>NU1902;NU1903</WarningsNotAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -19,9 +19,4 @@
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" />
</ItemGroup> </ItemGroup>
<!-- Custom Analyzers -->
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' AND '$(Configuration)' == 'Debug' ">
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj" OutputItemType="Analyzer" />
</ItemGroup>
</Project> </Project>

View File

@@ -4,96 +4,88 @@
</PropertyGroup> </PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies"> <ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="7.1.7" /> <PackageVersion Include="AsyncKeyedLock" Version="7.0.2" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" /> <PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp" Version="1.3.4" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" /> <PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Diacritics" Version="4.0.17" /> <PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.2" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" /> <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Ignore" Version="0.2.1" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="libse" Version="4.0.8" />
<PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="LrcParser" Version="2024.0728.2" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.11" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.11" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.11" /> <PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.11" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="1.1.0.5" /> <PackageVersion Include="NEbml" Version="0.12.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" /> <PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" /> <PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Polly" Version="8.6.5" /> <PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" /> <PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.2.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" />
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 --> <PackageVersion Include="SkiaSharp" Version="2.88.9" />
<PackageVersion Include="SkiaSharp" Version="3.116.1" /> <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.2.1" /> <PackageVersion Include="Svg.Skia" Version="2.0.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="9.0.11" /> <PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.9.0" /> <PackageVersion Include="z440.atl.core" Version="6.16.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" /> <PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.9.3" /> <PackageVersion Include="xunit" Version="2.9.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -21,8 +21,8 @@ namespace Emby.Naming.Common
/// </summary> /// </summary>
public NamingOptions() public NamingOptions()
{ {
VideoFileExtensions = VideoFileExtensions = new[]
[ {
".001", ".001",
".3g2", ".3g2",
".3gp", ".3gp",
@@ -77,10 +77,10 @@ namespace Emby.Naming.Common
".wmv", ".wmv",
".wtv", ".wtv",
".xvid" ".xvid"
]; };
VideoFlagDelimiters = VideoFlagDelimiters = new[]
[ {
'(', '(',
')', ')',
'-', '-',
@@ -88,15 +88,15 @@ namespace Emby.Naming.Common
'_', '_',
'[', '[',
']' ']'
]; };
StubFileExtensions = StubFileExtensions = new[]
[ {
".disc" ".disc"
]; };
StubTypes = StubTypes = new[]
[ {
new StubTypeRule( new StubTypeRule(
stubType: "dvd", stubType: "dvd",
token: "dvd"), token: "dvd"),
@@ -136,32 +136,32 @@ namespace Emby.Naming.Common
new StubTypeRule( new StubTypeRule(
stubType: "tv", stubType: "tv",
token: "DSR") token: "DSR")
]; };
VideoFileStackingRules = VideoFileStackingRules = new[]
[ {
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true), new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false) new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
]; };
CleanDateTimes = CleanDateTimes = new[]
[ {
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*", @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*" @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
]; };
CleanStrings = CleanStrings = new[]
[ {
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)", @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"^(?<cleaned>.+?)(\[.*\])", @"^(?<cleaned>.+?)(\[.*\])",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$", @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$" @"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
]; };
SubtitleFileExtensions = SubtitleFileExtensions = new[]
[ {
".ass", ".ass",
".mks", ".mks",
".sami", ".sami",
@@ -171,29 +171,27 @@ namespace Emby.Naming.Common
".sub", ".sub",
".sup", ".sup",
".vtt", ".vtt",
]; };
LyricFileExtensions = LyricFileExtensions = new[]
[ {
".lrc", ".lrc",
".elrc", ".elrc",
".txt" ".txt"
]; };
AlbumStackingPrefixes = AlbumStackingPrefixes = new[]
[ {
"cd", "cd",
"digital media", "digital media",
"disc", "disc",
"disk", "disk",
"vol", "vol",
"volume", "volume"
"part", };
"act"
];
ArtistSubfolders = ArtistSubfolders = new[]
[ {
"albums", "albums",
"broadcasts", "broadcasts",
"bootlegs", "bootlegs",
@@ -208,10 +206,10 @@ namespace Emby.Naming.Common
"soundtracks", "soundtracks",
"spokenwords", "spokenwords",
"streets" "streets"
]; };
AudioFileExtensions = AudioFileExtensions = new[]
[ {
".669", ".669",
".3gp", ".3gp",
".aa", ".aa",
@@ -240,8 +238,6 @@ namespace Emby.Naming.Common
".dsp", ".dsp",
".dts", ".dts",
".dvf", ".dvf",
".eac3",
".ec3",
".far", ".far",
".flac", ".flac",
".gdm", ".gdm",
@@ -292,33 +288,33 @@ namespace Emby.Naming.Common
".xm", ".xm",
".xsp", ".xsp",
".ymf" ".ymf"
]; };
MediaFlagDelimiters = MediaFlagDelimiters = new[]
[ {
'.' '.'
]; };
MediaForcedFlags = MediaForcedFlags = new[]
[ {
"foreign", "foreign",
"forced" "forced"
]; };
MediaDefaultFlags = MediaDefaultFlags = new[]
[ {
"default" "default"
]; };
MediaHearingImpairedFlags = MediaHearingImpairedFlags = new[]
[ {
"cc", "cc",
"hi", "hi",
"sdh" "sdh"
]; };
EpisodeExpressions = EpisodeExpressions = new[]
[ {
// *** Begin Kodi Standard Naming // *** Begin Kodi Standard Naming
// <!-- foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02 --> // <!-- foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02 -->
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$") new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$")
@@ -331,23 +327,23 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"), new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true) new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
{ {
DateTimeFormats = DateTimeFormats = new[]
[ {
"yyyy.MM.dd", "yyyy.MM.dd",
"yyyy-MM-dd", "yyyy-MM-dd",
"yyyy_MM_dd", "yyyy_MM_dd",
"yyyy MM dd" "yyyy MM dd"
] }
}, },
new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true) new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
{ {
DateTimeFormats = DateTimeFormats = new[]
[ {
"dd.MM.yyyy", "dd.MM.yyyy",
"dd-MM-yyyy", "dd-MM-yyyy",
"dd_MM_yyyy", "dd_MM_yyyy",
"dd MM yyyy" "dd MM yyyy"
] }
}, },
// This isn't a Kodi naming rule, but the expression below causes false episode numbers for // This isn't a Kodi naming rule, but the expression below causes false episode numbers for
@@ -471,18 +467,10 @@ namespace Emby.Naming.Common
{ {
IsNamed = true IsNamed = true
}, },
};
// Anime style expression VideoExtraRules = new[]
// "[Group][Series Name][21][1080p][FLAC][HASH]" {
// "[Group] Series Name [04][BDRIP]"
new EpisodeExpression(@"(?:\[(?:[^\]]+)\]\s*)?(?<seriesname>\[[^\]]+\]|[^[\]]+)\s*\[(?<epnumber>[0-9]+)\]")
{
IsNamed = true
},
];
VideoExtraRules =
[
new ExtraRule( new ExtraRule(
ExtraType.Trailer, ExtraType.Trailer,
ExtraRuleType.DirectoryName, ExtraRuleType.DirectoryName,
@@ -573,18 +561,6 @@ namespace Emby.Naming.Common
"trailer", "trailer",
MediaType.Video), MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Filename,
"sample",
MediaType.Video),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule( new ExtraRule(
ExtraType.Trailer, ExtraType.Trailer,
ExtraRuleType.Suffix, ExtraRuleType.Suffix,
@@ -606,7 +582,13 @@ namespace Emby.Naming.Common
new ExtraRule( new ExtraRule(
ExtraType.Trailer, ExtraType.Trailer,
ExtraRuleType.Suffix, ExtraRuleType.Suffix,
"- trailer", " trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Filename,
"sample",
MediaType.Video), MediaType.Video),
new ExtraRule( new ExtraRule(
@@ -630,9 +612,15 @@ namespace Emby.Naming.Common
new ExtraRule( new ExtraRule(
ExtraType.Sample, ExtraType.Sample,
ExtraRuleType.Suffix, ExtraRuleType.Suffix,
"- sample", " sample",
MediaType.Video), MediaType.Video),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule( new ExtraRule(
ExtraType.Scene, ExtraType.Scene,
ExtraRuleType.Suffix, ExtraRuleType.Suffix,
@@ -692,14 +680,14 @@ namespace Emby.Naming.Common
ExtraRuleType.Suffix, ExtraRuleType.Suffix,
"-other", "-other",
MediaType.Video) MediaType.Video)
]; };
AllExtrasTypesFolderNames = VideoExtraRules AllExtrasTypesFolderNames = VideoExtraRules
.Where(i => i.RuleType == ExtraRuleType.DirectoryName) .Where(i => i.RuleType == ExtraRuleType.DirectoryName)
.ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase); .ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase);
Format3DRules = Format3DRules = new[]
[ {
// Kodi rules: // Kodi rules:
new Format3DRule( new Format3DRule(
precedingToken: "3d", precedingToken: "3d",
@@ -726,10 +714,10 @@ namespace Emby.Naming.Common
new Format3DRule("tab"), new Format3DRule("tab"),
new Format3DRule("sbs3d"), new Format3DRule("sbs3d"),
new Format3DRule("mvc") new Format3DRule("mvc")
]; };
AudioBookPartsExpressions = AudioBookPartsExpressions = new[]
[ {
// Detect specified chapters, like CH 01 // Detect specified chapters, like CH 01
@"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)", @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
// Detect specified parts, like Part 02 // Detect specified parts, like Part 02
@@ -742,14 +730,14 @@ namespace Emby.Naming.Common
"(?<chapter>[0-9]+)_(?<part>[0-9]+)", "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number. // Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)" @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
]; };
AudioBookNamesExpressions = AudioBookNamesExpressions = new[]
[ {
// Detect year usually in brackets after name Batman (2020) // Detect year usually in brackets after name Batman (2020)
@"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$", @"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$",
@"^\s*(?<name>[^ ].*?)\s*$" @"^\s*(?<name>[^ ].*?)\s*$"
]; };
MultipleEpisodeExpressions = new[] MultipleEpisodeExpressions = new[]
{ {
@@ -889,12 +877,12 @@ namespace Emby.Naming.Common
/// <summary> /// <summary>
/// Gets list of clean datetime regular expressions. /// Gets list of clean datetime regular expressions.
/// </summary> /// </summary>
public Regex[] CleanDateTimeRegexes { get; private set; } = []; public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary> /// <summary>
/// Gets list of clean string regular expressions. /// Gets list of clean string regular expressions.
/// </summary> /// </summary>
public Regex[] CleanStringRegexes { get; private set; } = []; public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary> /// <summary>
/// Compiles raw regex strings into regexes. /// Compiles raw regex strings into regexes.

View File

@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -36,7 +36,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.12.0</VersionPrefix> <VersionPrefix>10.10.6</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@@ -97,18 +97,14 @@ namespace Emby.Naming.ExternalFiles
if (culture is not null && pathInfo.Language is null) if (culture is not null && pathInfo.Language is null)
{ {
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) pathInfo.Language = culture.ThreeLetterISOLanguageName;
? culture.Name
: culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
} }
else if (culture is not null && pathInfo.Language == "hin") else if (culture is not null && pathInfo.Language == "hin")
{ {
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set // Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
pathInfo.IsHearingImpaired = true; pathInfo.IsHearingImpaired = true;
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) pathInfo.Language = culture.ThreeLetterISOLanguageName;
? culture.Name
: culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
} }
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase))) else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))

View File

@@ -1,40 +1,43 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
namespace Emby.Naming.TV namespace Emby.Naming.TV
{ {
/// <summary> /// <summary>
/// Class to parse season paths. /// Class to parse season paths.
/// </summary> /// </summary>
public static partial class SeasonPathParser public static class SeasonPathParser
{ {
private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled); /// <summary>
/// A season folder must contain one of these somewhere in the name.
/// </summary>
private static readonly string[] _seasonFolderNames =
{
"season",
"sæson",
"temporada",
"saison",
"staffel",
"series",
"сезон",
"stagione"
};
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)] private static readonly char[] _splitChars = ['.', '_', ' ', '-'];
private static partial Regex ProcessPre();
[GeneratedRegex(@"^\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPost();
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
private static partial Regex SeasonPrefix();
/// <summary> /// <summary>
/// Attempts to parse season number from path. /// Attempts to parse season number from path.
/// </summary> /// </summary>
/// <param name="path">Path to season.</param> /// <param name="path">Path to season.</param>
/// <param name="parentPath">Folder name of the parent.</param>
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param> /// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param> /// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns> /// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders) public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{ {
var result = new SeasonPathParserResult(); var result = new SeasonPathParserResult();
var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders); var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
result.SeasonNumber = seasonNumber; result.SeasonNumber = seasonNumber;
@@ -51,70 +54,84 @@ namespace Emby.Naming.TV
/// Gets the season number from path. /// Gets the season number from path.
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="parentFolderName">The parent folder name.</param>
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param> /// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param> /// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
/// <returns>System.Nullable{System.Int32}.</returns> /// <returns>System.Nullable{System.Int32}.</returns>
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath( private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
string path, string path,
string? parentFolderName,
bool supportSpecialAliases, bool supportSpecialAliases,
bool supportNumericSeasonFolders) bool supportNumericSeasonFolders)
{ {
var fileName = Path.GetFileName(path); string filename = Path.GetFileName(path);
var seasonPrefixMatch = SeasonPrefix().Match(fileName); if (supportSpecialAliases)
if (seasonPrefixMatch.Success &&
int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{ {
return (val, true); if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase))
{
return (0, true);
}
if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase))
{
return (0, true);
}
} }
string filename = CleanNameRegex.Replace(fileName, string.Empty); if (supportNumericSeasonFolders)
if (parentFolderName is not null)
{ {
var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty); if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase); {
return (val, true);
}
} }
if (supportSpecialAliases && if (TryGetSeasonNumberFromPart(filename, out int seasonNumber))
(filename.Equals("specials", StringComparison.OrdinalIgnoreCase) ||
filename.Equals("extras", StringComparison.OrdinalIgnoreCase)))
{ {
return (0, true); return (seasonNumber, true);
} }
if (supportNumericSeasonFolders && // Look for one of the season folder names
int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) foreach (var name in _seasonFolderNames)
{ {
return (val, true); if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
{
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
if (result.SeasonNumber.HasValue)
{
return result;
}
break;
}
} }
var preMatch = ProcessPre().Match(filename); var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries);
if (preMatch.Success) foreach (var part in parts)
{ {
return CheckMatch(preMatch); if (TryGetSeasonNumberFromPart(part, out seasonNumber))
}
else
{
var postMatch = ProcessPost().Match(filename);
return CheckMatch(postMatch);
}
}
private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
{
var numberString = match.Groups["seasonnumber"];
if (numberString.Success)
{
if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber))
{ {
return (seasonNumber, true); return (seasonNumber, true);
} }
} }
return (null, false); return (null, true);
}
private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber)
{
seasonNumber = 0;
if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
seasonNumber = value;
return true;
}
return false;
} }
/// <summary> /// <summary>

View File

@@ -12,18 +12,11 @@ namespace Emby.Naming.TV
/// <summary> /// <summary>
/// Regex that matches strings of at least 2 characters separated by a dot or underscore. /// Regex that matches strings of at least 2 characters separated by a dot or underscore.
/// Used for removing separators between words, i.e turns "The_show" into "The show" while /// Used for removing separators between words, i.e turns "The_show" into "The show" while
/// preserving names like "S.H.O.W". /// preserving namings like "S.H.O.W".
/// </summary> /// </summary>
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")] [GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
private static partial Regex SeriesNameRegex(); private static partial Regex SeriesNameRegex();
/// <summary>
/// Regex that matches titles with year in parentheses. Captures the title (which may be
/// numeric) before the year, i.e. turns "1923 (2022)" into "1923".
/// </summary>
[GeneratedRegex(@"(?<title>.+?)\s*\(\d{4}\)")]
private static partial Regex TitleWithYearRegex();
/// <summary> /// <summary>
/// Resolve information about series from path. /// Resolve information about series from path.
/// </summary> /// </summary>
@@ -34,20 +27,6 @@ namespace Emby.Naming.TV
{ {
string seriesName = Path.GetFileName(path); string seriesName = Path.GetFileName(path);
// First check if the filename matches a title with year pattern (handles numeric titles)
if (!string.IsNullOrEmpty(seriesName))
{
var titleWithYearMatch = TitleWithYearRegex().Match(seriesName);
if (titleWithYearMatch.Success)
{
seriesName = titleWithYearMatch.Groups["title"].Value.Trim();
return new SeriesInfo(path)
{
Name = seriesName
};
}
}
SeriesPathParserResult result = SeriesPathParser.Parse(options, path); SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
if (result.Success) if (result.Success)
{ {

View File

@@ -18,49 +18,68 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <param name="path">Path to file.</param> /// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns> /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "") public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
{ {
ExtraResult result = new ExtraResult(); var result = new ExtraResult();
bool isAudioFile = AudioFileParser.IsAudioFile(path, namingOptions); for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
bool isVideoFile = VideoResolver.IsVideoFile(path, namingOptions);
ReadOnlySpan<char> pathSpan = path.AsSpan();
ReadOnlySpan<char> fileName = Path.GetFileName(pathSpan);
ReadOnlySpan<char> fileNameWithoutExtension = Path.GetFileNameWithoutExtension(pathSpan);
// Trim the digits from the end of the filename so we can recognize things like -trailer2
ReadOnlySpan<char> trimmedFileNameWithoutExtension = fileNameWithoutExtension.TrimEnd(_digits);
ReadOnlySpan<char> directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
foreach (ExtraRule rule in namingOptions.VideoExtraRules)
{ {
if ((rule.MediaType == MediaType.Audio && !isAudioFile) var rule = namingOptions.VideoExtraRules[i];
|| (rule.MediaType == MediaType.Video && !isVideoFile)) if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
{ {
continue; continue;
} }
bool isMatch = rule.RuleType switch var pathSpan = path.AsSpan();
if (rule.RuleType == ExtraRuleType.Filename)
{ {
ExtraRuleType.Filename => fileNameWithoutExtension.Equals(rule.Token, StringComparison.OrdinalIgnoreCase), var filename = Path.GetFileNameWithoutExtension(pathSpan);
ExtraRuleType.Suffix => trimmedFileNameWithoutExtension.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase),
ExtraRuleType.Regex => Regex.IsMatch(fileName, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled),
ExtraRuleType.DirectoryName => directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase),
_ => false,
};
if (!isMatch) if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{ {
continue; // Trim the digits from the end of the filename so we can recognize things like -trailer2
var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
var filename = Path.GetFileName(path.AsSpan());
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
if (isMatch)
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
} }
result.ExtraType = rule.ExtraType; if (result.ExtraType is not null)
result.Rule = rule; {
return result; return result;
}
} }
return result; return result;

View File

@@ -132,7 +132,7 @@ namespace Emby.Naming.Video
} }
} }
private sealed class StackMetadata private class StackMetadata
{ {
public StackMetadata(bool isDirectory, bool isNumerical, string partType) public StackMetadata(bool isDirectory, bool isNumerical, string partType)
{ {

View File

@@ -27,9 +27,8 @@ namespace Emby.Naming.Video
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param> /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param> /// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns> /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "") public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
{ {
// Filter out all extras, otherwise they could cause stacks to not be resolved // Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer // See the unit test TestStackedWithTrailer
@@ -66,7 +65,7 @@ namespace Emby.Naming.Video
{ {
var info = new VideoInfo(stack.Name) var info = new VideoInfo(stack.Name)
{ {
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot)) Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
.OfType<VideoFileInfo>() .OfType<VideoFileInfo>()
.ToList() .ToList()
}; };

View File

@@ -17,11 +17,10 @@ namespace Emby.Naming.Video
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param> /// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>VideoFileInfo.</returns> /// <returns>VideoFileInfo.</returns>
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "") public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
{ {
return Resolve(path, true, namingOptions, parseName, libraryRoot); return Resolve(path, true, namingOptions, parseName);
} }
/// <summary> /// <summary>
@@ -29,11 +28,10 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>VideoFileInfo.</returns> /// <returns>VideoFileInfo.</returns>
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions, string? libraryRoot = "") public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
{ {
return Resolve(path, false, namingOptions, libraryRoot: libraryRoot); return Resolve(path, false, namingOptions);
} }
/// <summary> /// <summary>
@@ -43,10 +41,9 @@ namespace Emby.Naming.Video
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param> /// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether or not the name should be parsed for info.</param> /// <param name="parseName">Whether or not the name should be parsed for info.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>VideoFileInfo.</returns> /// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception> /// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "") public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
@@ -78,7 +75,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions); var format3DResult = Format3DParser.Parse(path, namingOptions);
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions, libraryRoot); var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
var name = Path.GetFileNameWithoutExtension(path); var name = Path.GetFileNameWithoutExtension(path);

View File

@@ -19,7 +19,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>

View File

@@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
if (item.IsFileProtocol) if (item.IsFileProtocol)
{ {
var file = directoryService.GetFile(item.Path); var file = directoryService.GetFile(item.Path);
return file is not null && item.HasChanged(file.LastWriteTimeUtc); return file is not null && file.LastWriteTimeUtc != item.DateModified;
} }
return false; return false;
@@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
var dateTaken = image.ImageTag.DateTime; var dateTaken = image.ImageTag.DateTime;
if (dateTaken.HasValue) if (dateTaken.HasValue)
{ {
item.DateCreated = dateTaken.Value.ToUniversalTime(); item.DateCreated = dateTaken.Value;
item.PremiereDate = dateTaken.Value; item.PremiereDate = dateTaken.Value;
item.ProductionYear = dateTaken.Value.Year; item.ProductionYear = dateTaken.Value.Year;
} }

View File

@@ -1,8 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
namespace Emby.Server.Implementations.AppBase namespace Emby.Server.Implementations.AppBase
@@ -33,101 +30,80 @@ namespace Emby.Server.Implementations.AppBase
ConfigurationDirectoryPath = configurationDirectoryPath; ConfigurationDirectoryPath = configurationDirectoryPath;
CachePath = cacheDirectoryPath; CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath; WebPath = webDirectoryPath;
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
} }
/// <inheritdoc/> /// <summary>
/// Gets the path to the program data folder.
/// </summary>
/// <value>The program data path.</value>
public string ProgramDataPath { get; } public string ProgramDataPath { get; }
/// <inheritdoc/> /// <inheritdoc/>
public string WebPath { get; } public string WebPath { get; }
/// <inheritdoc/> /// <summary>
/// Gets the path to the system folder.
/// </summary>
/// <value>The path to the system folder.</value>
public string ProgramSystemPath { get; } = AppContext.BaseDirectory; public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
/// <inheritdoc/> /// <summary>
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
public string DataPath { get; } public string DataPath { get; }
/// <inheritdoc /> /// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%"; public string VirtualDataPath => "%AppDataPath%";
/// <inheritdoc/> /// <summary>
/// Gets the image cache path.
/// </summary>
/// <value>The image cache path.</value>
public string ImageCachePath => Path.Combine(CachePath, "images"); public string ImageCachePath => Path.Combine(CachePath, "images");
/// <inheritdoc/> /// <summary>
/// Gets the path to the plugin directory.
/// </summary>
/// <value>The plugins path.</value>
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins"); public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
/// <inheritdoc/> /// <summary>
/// Gets the path to the plugin configurations directory.
/// </summary>
/// <value>The plugin configurations path.</value>
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations"); public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
/// <inheritdoc/> /// <summary>
/// Gets the path to the log directory.
/// </summary>
/// <value>The log directory path.</value>
public string LogDirectoryPath { get; } public string LogDirectoryPath { get; }
/// <inheritdoc/> /// <summary>
/// Gets the path to the application configuration root directory.
/// </summary>
/// <value>The configuration directory path.</value>
public string ConfigurationDirectoryPath { get; } public string ConfigurationDirectoryPath { get; }
/// <inheritdoc/> /// <summary>
/// Gets the path to the system configuration file.
/// </summary>
/// <value>The system configuration file path.</value>
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml"); public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
/// <inheritdoc/> /// <summary>
/// Gets or sets the folder path to the cache directory.
/// </summary>
/// <value>The cache directory.</value>
public string CachePath { get; set; } public string CachePath { get; set; }
/// <inheritdoc/> /// <summary>
/// Gets the folder path to the temp directory within the cache folder.
/// </summary>
/// <value>The temp directory.</value>
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin"); public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
/// <inheritdoc />
public string TrickplayPath => Path.Combine(DataPath, "trickplay");
/// <inheritdoc />
public string BackupPath => Path.Combine(DataPath, "backups");
/// <inheritdoc />
public virtual void MakeSanityCheckOrThrow()
{
CreateAndCheckMarker(ConfigurationDirectoryPath, "config");
CreateAndCheckMarker(LogDirectoryPath, "log");
CreateAndCheckMarker(PluginsPath, "plugin");
CreateAndCheckMarker(ProgramDataPath, "data");
CreateAndCheckMarker(CachePath, "cache");
CreateAndCheckMarker(DataPath, "data");
}
/// <inheritdoc />
public void CreateAndCheckMarker(string path, string markerName, bool recursive = false)
{
Directory.CreateDirectory(path);
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
}
private IEnumerable<string> GetMarkers(string path, bool recursive = false)
{
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
}
private void CheckOrCreateMarker(string path, string markerName, bool recursive = false)
{
string? otherMarkers = null;
try
{
otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => !Path.GetFileName(e.AsSpan()).Equals(markerName, StringComparison.OrdinalIgnoreCase));
}
catch
{
// Error while checking for marker files, assume none exist and keep going
// TODO: add some logging
}
if (otherMarkers is not null)
{
throw new InvalidOperationException($"Expected to find only {markerName} but found marker for {otherMarkers}.");
}
var markerPath = Path.Combine(path, markerName);
if (!File.Exists(markerPath))
{
FileHelper.CreateEmpty(markerPath);
}
}
} }
} }

View File

@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events; using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@@ -20,7 +19,7 @@ namespace Emby.Server.Implementations.AppBase
public abstract class BaseConfigurationManager : IConfigurationManager public abstract class BaseConfigurationManager : IConfigurationManager
{ {
private readonly ConcurrentDictionary<string, object> _configurations = new(); private readonly ConcurrentDictionary<string, object> _configurations = new();
private readonly Lock _configurationSyncLock = new(); private readonly object _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -227,7 +226,6 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Setting cache path: {Path}", cachePath); Logger.LogInformation("Setting cache path: {Path}", cachePath);
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath; ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
} }
/// <summary> /// <summary>

View File

@@ -15,7 +15,6 @@ using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Photos; using Emby.Photos;
using Emby.Server.Implementations.Chapters;
using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Collections;
using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Configuration;
using Emby.Server.Implementations.Cryptography; using Emby.Server.Implementations.Cryptography;
@@ -40,10 +39,8 @@ using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager; using Jellyfin.Networking.Manager;
using Jellyfin.Networking.Udp; using Jellyfin.Networking.Udp;
using Jellyfin.Server.Implementations.FullSystemBackup; using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.Implementations.MediaSegments; using Jellyfin.Server.Implementations.MediaSegments;
using Jellyfin.Server.Implementations.SystemBackupService;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events; using MediaBrowser.Common.Events;
@@ -59,14 +56,10 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LibraryTaskScheduler;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
@@ -90,6 +83,7 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Lyric; using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Plugins.Tmdb;
@@ -97,6 +91,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -271,15 +266,8 @@ namespace Emby.Server.Implementations
? Environment.MachineName ? Environment.MachineName
: ConfigurationManager.Configuration.ServerName; : ConfigurationManager.Configuration.ServerName;
public string RestoreBackupPath { get; set; }
public string ExpandVirtualPath(string path) public string ExpandVirtualPath(string path)
{ {
if (path is null)
{
return null;
}
var appPaths = ApplicationPaths; var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase) return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
@@ -477,7 +465,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IApplicationHost>(this); serviceCollection.AddSingleton<IApplicationHost>(this);
serviceCollection.AddSingleton<IPluginManager>(_pluginManager); serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton<IBackupService, BackupService>();
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>(); serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>(); serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
@@ -505,20 +492,13 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>(); serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>();
serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>(); serviceCollection.AddSingleton<EncodingHelper>();
serviceCollection.AddSingleton<IPathManager, PathManager>();
serviceCollection.AddSingleton<IExternalDataManager, ExternalDataManager>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@@ -553,7 +533,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISessionManager, SessionManager>(); serviceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
serviceCollection.AddSingleton<ILimitedConcurrencyLibraryScheduler, LimitedConcurrencyLibraryScheduler>();
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
@@ -563,12 +542,13 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
serviceCollection.AddSingleton<IAuthService, AuthService>(); serviceCollection.AddSingleton<IAuthService, AuthService>();
serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>(); serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>(); serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
serviceCollection.AddSingleton<IKeyframeManager, KeyframeManager>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
@@ -585,10 +565,23 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Create services registered with the service container that need to be initialized at application startup. /// Create services registered with the service container that need to be initialized at application startup.
/// </summary> /// </summary>
/// <param name="startupConfig">The configuration used to initialise the application.</param>
/// <returns>A task representing the service initialization operation.</returns> /// <returns>A task representing the service initialization operation.</returns>
public async Task InitializeServices(IConfiguration startupConfig) public async Task InitializeServices()
{ {
var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
await using (jellyfinDb.ConfigureAwait(false))
{
if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
{
Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false);
Logger.LogInformation("EFCore migrations applied successfully");
}
}
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false); await localizationManager.LoadAll().ConfigureAwait(false);
@@ -614,7 +607,7 @@ namespace Emby.Server.Implementations
// Don't use an empty string password // Don't use an empty string password
password = string.IsNullOrWhiteSpace(password) ? null : password; password = string.IsNullOrWhiteSpace(password) ? null : password;
var localCert = X509CertificateLoader.LoadPkcs12FromFile(path, password, X509KeyStorageFlags.UserKeySet); var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
if (!localCert.HasPrivateKey) if (!localCert.HasPrivateKey)
{ {
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path); Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
@@ -636,26 +629,23 @@ namespace Emby.Server.Implementations
private void SetStaticProperties() private void SetStaticProperties()
{ {
// For now there's no real way to inject these properly // For now there's no real way to inject these properly
BaseItem.ChapterManager = Resolve<IChapterManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.Logger = Resolve<ILogger<BaseItem>>(); BaseItem.Logger = Resolve<ILogger<BaseItem>>();
BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>(); BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>(); BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.ProviderManager = Resolve<IProviderManager>(); BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>(); BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
Video.RecordingsManager = Resolve<IRecordingsManager>();
Folder.UserViewManager = Resolve<IUserViewManager>();
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
UserView.CollectionManager = Resolve<ICollectionManager>();
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
CollectionFolder.XmlSerializer = _xmlSerializer; CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.ApplicationHost = this; CollectionFolder.ApplicationHost = this;
Folder.UserViewManager = Resolve<IUserViewManager>();
Folder.CollectionManager = Resolve<ICollectionManager>();
Folder.LimitedConcurrencyLibraryScheduler = Resolve<ILimitedConcurrencyLibraryScheduler>();
Episode.MediaEncoder = Resolve<IMediaEncoder>();
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
Video.RecordingsManager = Resolve<IRecordingsManager>();
} }
/// <summary> /// <summary>

View File

@@ -1,300 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Chapters;
/// <summary>
/// The chapter manager.
/// </summary>
public class ChapterManager : IChapterManager
{
private readonly IFileSystem _fileSystem;
private readonly ILogger<ChapterManager> _logger;
private readonly IMediaEncoder _encoder;
private readonly IChapterRepository _chapterRepository;
private readonly ILibraryManager _libraryManager;
private readonly IPathManager _pathManager;
/// <summary>
/// The first chapter ticks.
/// </summary>
private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
/// <summary>
/// Initializes a new instance of the <see cref="ChapterManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{ChapterManager}"/>.</param>
/// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
/// <param name="encoder">The <see cref="IMediaEncoder"/>.</param>
/// <param name="chapterRepository">The <see cref="IChapterRepository"/>.</param>
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
/// <param name="pathManager">The <see cref="IPathManager"/>.</param>
public ChapterManager(
ILogger<ChapterManager> logger,
IFileSystem fileSystem,
IMediaEncoder encoder,
IChapterRepository chapterRepository,
ILibraryManager libraryManager,
IPathManager pathManager)
{
_logger = logger;
_fileSystem = fileSystem;
_encoder = encoder;
_chapterRepository = chapterRepository;
_libraryManager = libraryManager;
_pathManager = pathManager;
}
/// <summary>
/// Determines whether [is eligible for chapter image extraction] [the specified video].
/// </summary>
/// <param name="video">The video.</param>
/// <param name="libraryOptions">The library options for the video.</param>
/// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns>
private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions)
{
if (video.IsPlaceHolder)
{
return false;
}
if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction)
{
return false;
}
if (video.IsShortcut)
{
return false;
}
if (!video.IsCompleteMedia)
{
return false;
}
// Can't extract images if there are no video streams
return video.DefaultVideoStreamIndex.HasValue;
}
private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters)
{
if (chapters.Count < 2)
{
return 0;
}
long sum = 0;
for (int i = 1; i < chapters.Count; i++)
{
sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks;
}
return sum / chapters.Count;
}
/// <inheritdoc />
public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
{
if (chapters.Count == 0)
{
return true;
}
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (!IsEligibleForChapterImageExtraction(video, libraryOptions))
{
extractImages = false;
}
var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
var threshold = TimeSpan.FromSeconds(1).Ticks;
if (averageChapterDuration < threshold)
{
_logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
extractImages = false;
}
var success = true;
var changesMade = false;
var runtimeTicks = video.RunTimeTicks ?? 0;
var currentImages = GetSavedChapterImages(video, directoryService);
foreach (var chapter in chapters)
{
if (chapter.StartPositionTicks >= runtimeTicks)
{
_logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name);
break;
}
var path = _pathManager.GetChapterImagePath(video, chapter.StartPositionTicks);
if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase))
{
if (extractImages)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
// Add some time for the first chapter to make sure we don't end up with a black image
var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
var inputPath = video.Path;
var directoryPath = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
var container = video.Container;
var mediaSource = new MediaSourceInfo
{
VideoType = video.VideoType,
IsoType = video.IsoType,
Protocol = video.PathProtocol ?? MediaProtocol.File,
};
_logger.LogInformation("Extracting chapter image for {Name} at {Path}", video.Name, inputPath);
var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false);
File.Copy(tempFile, path, true);
try
{
_fileSystem.DeleteFile(tempFile);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile);
}
chapter.ImagePath = path;
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
changesMade = true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path));
success = false;
break;
}
}
else if (!string.IsNullOrEmpty(chapter.ImagePath))
{
chapter.ImagePath = null;
changesMade = true;
}
}
else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase))
{
chapter.ImagePath = path;
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
changesMade = true;
}
else if (libraryOptions?.EnableChapterImageExtraction != true)
{
// We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image
chapter.ImagePath = null;
changesMade = true;
}
}
if (saveChapters && changesMade)
{
SaveChapters(video, chapters);
}
DeleteDeadImages(currentImages, chapters);
return success;
}
/// <inheritdoc />
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
{
// Remove any chapters that are outside of the runtime of the video
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
_chapterRepository.SaveChapters(video.Id, validChapters);
}
/// <inheritdoc />
public ChapterInfo? GetChapter(Guid baseItemId, int index)
{
return _chapterRepository.GetChapter(baseItemId, index);
}
/// <inheritdoc />
public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId)
{
return _chapterRepository.GetChapters(baseItemId);
}
/// <inheritdoc />
public async Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken)
{
await _chapterRepository.DeleteChaptersAsync(itemId, cancellationToken).ConfigureAwait(false);
}
private IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
{
var path = _pathManager.GetChapterImageFolderPath(video);
if (!Directory.Exists(path))
{
return [];
}
try
{
return directoryService.GetFilePaths(path);
}
catch (IOException)
{
return [];
}
}
private void DeleteDeadImages(IEnumerable<string> images, IEnumerable<ChapterInfo> chapters)
{
var existingImages = chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i));
var deadImages = images
.Except(existingImages, StringComparer.OrdinalIgnoreCase)
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var image in deadImages)
{
_logger.LogDebug("Deleting dead chapter image {Path}", image);
try
{
_fileSystem.DeleteFile(image!);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting {Path}.", image);
}
}
}
}

View File

@@ -4,7 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
var libraryOptions = new LibraryOptions var libraryOptions = new LibraryOptions
{ {
PathInfos = [new MediaPathInfo(path)], PathInfos = new[] { new MediaPathInfo(path) },
EnableRealtimeMonitor = false, EnableRealtimeMonitor = false,
SaveLocalMetadata = true SaveLocalMetadata = true
}; };
@@ -104,8 +104,6 @@ namespace Emby.Server.Implementations.Collections
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false); await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
_libraryManager.RootFolder.Children = null;
return FindFolders(path).First(); return FindFolders(path).First();
} }
@@ -152,15 +150,15 @@ namespace Emby.Server.Implementations.Collections
try try
{ {
var info = Directory.CreateDirectory(path); Directory.CreateDirectory(path);
var collection = new BoxSet var collection = new BoxSet
{ {
Name = name, Name = name,
Path = path, Path = path,
IsLocked = options.IsLocked, IsLocked = options.IsLocked,
ProviderIds = options.ProviderIds, ProviderIds = options.ProviderIds,
DateCreated = info.CreationTimeUtc, DateCreated = DateTime.UtcNow
DateModified = info.LastWriteTimeUtc
}; };
parentFolder.AddChild(collection); parentFolder.AddChild(collection);
@@ -206,7 +204,7 @@ namespace Emby.Server.Implementations.Collections
{ {
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection) if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{ {
throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId); throw new ArgumentException("No collection exists with the supplied Id");
} }
List<BaseItem>? itemList = null; List<BaseItem>? itemList = null;
@@ -220,7 +218,7 @@ namespace Emby.Server.Implementations.Collections
if (item is null) if (item is null)
{ {
throw new ArgumentException("No item exists with the supplied Id " + id); throw new ArgumentException("No item exists with the supplied Id");
} }
if (!currentLinkedChildrenIds.Contains(id)) if (!currentLinkedChildrenIds.Contains(id))

View File

@@ -0,0 +1,269 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Threading;
using Jellyfin.Extensions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
{
public abstract class BaseSqliteRepository : IDisposable
{
private bool _disposed = false;
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
private SqliteConnection _writeConnection;
/// <summary>
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
{
Logger = logger;
}
/// <summary>
/// Gets or sets the path to the DB file.
/// </summary>
protected string DbFilePath { get; set; }
/// <summary>
/// Gets the logger.
/// </summary>
/// <value>The logger.</value>
protected ILogger<BaseSqliteRepository> Logger { get; }
/// <summary>
/// Gets the cache size.
/// </summary>
/// <value>The cache size or null.</value>
protected virtual int? CacheSize => null;
/// <summary>
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
/// </summary>
protected virtual string LockingMode => "NORMAL";
/// <summary>
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
/// </summary>
/// <value>The journal mode.</value>
protected virtual string JournalMode => "WAL";
/// <summary>
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
/// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
/// </summary>
/// <value>The journal size limit.</value>
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
/// <summary>
/// Gets the page size.
/// </summary>
/// <value>The page size or null.</value>
protected virtual int? PageSize => null;
/// <summary>
/// Gets the temp store mode.
/// </summary>
/// <value>The temp store mode.</value>
/// <see cref="TempStoreMode"/>
protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
/// <summary>
/// Gets the synchronous mode.
/// </summary>
/// <value>The synchronous mode or null.</value>
/// <see cref="SynchronousMode"/>
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
public virtual void Initialize()
{
// Configuration and pragmas can affect VACUUM so it needs to be last.
using (var connection = GetConnection())
{
connection.Execute("VACUUM");
}
}
protected ManagedConnection GetConnection(bool readOnly = false)
{
if (!readOnly)
{
_writeLock.Wait();
if (_writeConnection is not null)
{
return new ManagedConnection(_writeConnection, _writeLock);
}
var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
writeConnection.Open();
if (CacheSize.HasValue)
{
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
}
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
}
var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
connection.Open();
if (CacheSize.HasValue)
{
connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
connection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
connection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
connection.Execute("PRAGMA page_size=" + PageSize.Value);
}
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return new ManagedConnection(connection, null);
}
public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
{
var command = connection.CreateCommand();
command.CommandText = sql;
return command;
}
protected bool TableExists(ManagedConnection connection, string name)
{
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
foreach (var row in statement.ExecuteQuery())
{
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
protected List<string> GetColumnNames(ManagedConnection connection, string table)
{
var columnNames = new List<string>();
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
{
if (row.TryGetString(1, out var columnName))
{
columnNames.Add(columnName);
}
}
return columnNames;
}
protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
return;
}
connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
}
protected void CheckDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (_disposed)
{
return;
}
if (dispose)
{
_writeLock.Wait();
try
{
_writeConnection.Dispose();
}
finally
{
_writeLock.Release();
}
_writeLock.Dispose();
}
_writeConnection = null;
_writeLock = null;
_disposed = true;
}
}
}

View File

@@ -1,119 +1,66 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data; namespace Emby.Server.Implementations.Data
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
{ {
private readonly ILibraryManager _libraryManager; public class CleanDatabaseScheduledTask : ILibraryPostScanTask
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IPathManager _pathManager;
public CleanDatabaseScheduledTask(
ILibraryManager libraryManager,
ILogger<CleanDatabaseScheduledTask> logger,
IDbContextFactory<JellyfinDbContext> dbProvider,
IPathManager pathManager)
{ {
_libraryManager = libraryManager; private readonly ILibraryManager _libraryManager;
_logger = logger; private readonly ILogger<CleanDatabaseScheduledTask> _logger;
_dbProvider = dbProvider;
_pathManager = pathManager;
}
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
{
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
}
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
{
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{ {
HasDeadParentId = true _libraryManager = libraryManager;
}); _logger = logger;
}
var numComplete = 0; public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
var numItems = itemIds.Count + 1;
_logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
foreach (var itemId in itemIds)
{ {
cancellationToken.ThrowIfCancellationRequested(); CleanDeadItems(cancellationToken, progress);
return Task.CompletedTask;
}
var item = _libraryManager.GetItemById(itemId); private void CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
if (item is not null) {
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{ {
_logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty); HasDeadParentId = true
});
foreach (var mediaSource in item.GetMediaSources(false)) var numComplete = 0;
var numItems = itemIds.Count;
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
foreach (var itemId in itemIds)
{
cancellationToken.ThrowIfCancellationRequested();
var item = _libraryManager.GetItemById(itemId);
if (item is not null)
{ {
// Delete extracted data _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
var mediaSourceItem = _libraryManager.GetItemById(mediaSource.Id);
if (mediaSourceItem is null)
{
continue;
}
var extractedDataFolders = _pathManager.GetExtractedDataPaths(mediaSourceItem); _libraryManager.DeleteItem(item, new DeleteOptions
foreach (var folder in extractedDataFolders)
{ {
if (Directory.Exists(folder)) DeleteFileLocation = false
{ });
try
{
Directory.Delete(folder, true);
}
catch (Exception e)
{
_logger.LogWarning("Failed to remove {Folder}: {Exception}", folder, e.Message);
}
}
}
} }
// Delete item numComplete++;
_libraryManager.DeleteItem(item, new DeleteOptions double percent = numComplete;
{ percent /= numItems;
DeleteFileLocation = false progress.Report(percent * 100);
});
} }
numComplete++; progress.Report(100);
double percent = numComplete;
percent /= numItems;
subProgress.Report(percent * 100);
} }
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
subProgress.Report(50);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
subProgress.Report(100);
}
}
progress.Report(100);
} }
} }

View File

@@ -1,64 +0,0 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Threading.Channels;
using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
namespace Emby.Server.Implementations.Data;
/// <inheritdoc />
public class ItemTypeLookup : IItemTypeLookup
{
/// <inheritdoc />
public IReadOnlyList<string> MusicGenreTypes { get; } = [
typeof(Audio).FullName!,
typeof(MusicVideo).FullName!,
typeof(MusicAlbum).FullName!,
typeof(MusicArtist).FullName!,
];
/// <inheritdoc />
public IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string>()
{
{ BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName! },
{ BaseItemKind.Audio, typeof(Audio).FullName! },
{ BaseItemKind.AudioBook, typeof(AudioBook).FullName! },
{ BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName! },
{ BaseItemKind.Book, typeof(Book).FullName! },
{ BaseItemKind.BoxSet, typeof(BoxSet).FullName! },
{ BaseItemKind.Channel, typeof(Channel).FullName! },
{ BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName! },
{ BaseItemKind.Episode, typeof(Episode).FullName! },
{ BaseItemKind.Folder, typeof(Folder).FullName! },
{ BaseItemKind.Genre, typeof(Genre).FullName! },
{ BaseItemKind.Movie, typeof(Movie).FullName! },
{ BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName! },
{ BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName! },
{ BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName! },
{ BaseItemKind.MusicArtist, typeof(MusicArtist).FullName! },
{ BaseItemKind.MusicGenre, typeof(MusicGenre).FullName! },
{ BaseItemKind.MusicVideo, typeof(MusicVideo).FullName! },
{ BaseItemKind.Person, typeof(Person).FullName! },
{ BaseItemKind.Photo, typeof(Photo).FullName! },
{ BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName! },
{ BaseItemKind.Playlist, typeof(Playlist).FullName! },
{ BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName! },
{ BaseItemKind.Season, typeof(Season).FullName! },
{ BaseItemKind.Series, typeof(Series).FullName! },
{ BaseItemKind.Studio, typeof(Studio).FullName! },
{ BaseItemKind.Trailer, typeof(Trailer).FullName! },
{ BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName! },
{ BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName! },
{ BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName! },
{ BaseItemKind.UserView, typeof(UserView).FullName! },
{ BaseItemKind.Video, typeof(Video).FullName! },
{ BaseItemKind.Year, typeof(Year).FullName! }
}.ToFrozenDictionary();
}

View File

@@ -0,0 +1,62 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Data.Sqlite;
namespace Emby.Server.Implementations.Data;
public sealed class ManagedConnection : IDisposable
{
private readonly SemaphoreSlim? _writeLock;
private SqliteConnection _db;
private bool _disposed = false;
public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
{
_db = db;
_writeLock = writeLock;
}
public SqliteTransaction BeginTransaction()
=> _db.BeginTransaction();
public SqliteCommand CreateCommand()
=> _db.CreateCommand();
public void Execute(string commandText)
=> _db.Execute(commandText);
public SqliteCommand PrepareStatement(string sql)
=> _db.PrepareStatement(sql);
public IEnumerable<SqliteDataReader> Query(string commandText)
=> _db.Query(commandText);
public void Dispose()
{
if (_disposed)
{
return;
}
if (_writeLock is null)
{
// Read connections are managed with an internal pool
_db.Dispose();
}
else
{
// Write lock is managed by BaseSqliteRepository
// Don't dispose here
_writeLock.Release();
}
_db = null!;
_disposed = true;
}
}

View File

@@ -127,16 +127,8 @@ namespace Emby.Server.Implementations.Data
return false; return false;
} }
try result = reader.GetGuid(index);
{ return true;
result = reader.GetGuid(index);
return true;
}
catch
{
result = Guid.Empty;
return false;
}
} }
public static bool TryGetString(this SqliteDataReader reader, int index, out string result) public static bool TryGetString(this SqliteDataReader reader, int index, out string result)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,369 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
{
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
{
private readonly IUserManager _userManager;
public SqliteUserDataRepository(
ILogger<SqliteUserDataRepository> logger,
IServerConfigurationManager config,
IUserManager userManager)
: base(logger)
{
_userManager = userManager;
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
}
/// <summary>
/// Opens the connection to the database.
/// </summary>
public override void Initialize()
{
base.Initialize();
using (var connection = GetConnection())
{
var userDatasTableExists = TableExists(connection, "UserDatas");
var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : _userManager.Users;
using var transaction = connection.BeginTransaction();
connection.Execute(string.Join(
';',
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
"drop index if exists idx_userdata",
"drop index if exists idx_userdata1",
"drop index if exists idx_userdata2",
"drop index if exists userdataindex1",
"drop index if exists userdataindex",
"drop index if exists userdataindex3",
"drop index if exists userdataindex4",
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
"create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
if (!userDataTableExists)
{
transaction.Commit();
return;
}
var existingColumnNames = GetColumnNames(connection, "userdata");
AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
if (userDatasTableExists)
{
return;
}
ImportUserIds(connection, users);
connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
transaction.Commit();
}
}
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
{
foreach (var user in users)
{
if (!userIdsWithUserData.Contains(user.Id))
{
continue;
}
statement.TryBind("@UserId", user.Id);
statement.TryBind("@InternalUserId", user.InternalId);
statement.ExecuteNonQuery();
}
}
}
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
{
var list = new List<Guid>();
using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
{
foreach (var row in statement.ExecuteQuery())
{
try
{
list.Add(row.GetGuid(0));
}
catch (Exception ex)
{
Logger.LogError(ex, "Error while getting user");
}
}
}
return list;
}
/// <inheritdoc />
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userData);
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
ArgumentException.ThrowIfNullOrEmpty(key);
PersistUserData(userId, key, userData, cancellationToken);
}
/// <inheritdoc />
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userData);
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
PersistAllUserData(userId, userData, cancellationToken);
}
/// <summary>
/// Persists the user data.
/// </summary>
/// <param name="internalUserId">The user id.</param>
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{
SaveUserData(connection, internalUserId, key, userData);
transaction.Commit();
}
}
private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
{
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{
statement.TryBind("@userId", internalUserId);
statement.TryBind("@key", key);
if (userData.Rating.HasValue)
{
statement.TryBind("@rating", userData.Rating.Value);
}
else
{
statement.TryBindNull("@rating");
}
statement.TryBind("@played", userData.Played);
statement.TryBind("@playCount", userData.PlayCount);
statement.TryBind("@isFavorite", userData.IsFavorite);
statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks);
if (userData.LastPlayedDate.HasValue)
{
statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue());
}
else
{
statement.TryBindNull("@lastPlayedDate");
}
if (userData.AudioStreamIndex.HasValue)
{
statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value);
}
else
{
statement.TryBindNull("@AudioStreamIndex");
}
if (userData.SubtitleStreamIndex.HasValue)
{
statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value);
}
else
{
statement.TryBindNull("@SubtitleStreamIndex");
}
statement.ExecuteNonQuery();
}
}
/// <summary>
/// Persist all user data for the specified user.
/// </summary>
private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{
foreach (var userItemData in userDataList)
{
SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
}
transaction.Commit();
}
}
/// <summary>
/// Gets the user data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="key">The key.</param>
/// <returns>Task{UserItemData}.</returns>
/// <exception cref="ArgumentNullException">
/// userId
/// or
/// key.
/// </exception>
public UserItemData GetUserData(long userId, string key)
{
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
ArgumentException.ThrowIfNullOrEmpty(key);
using (var connection = GetConnection(true))
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
statement.TryBind("@UserId", userId);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
{
return ReadRow(row);
}
}
return null;
}
}
public UserItemData GetUserData(long userId, List<string> keys)
{
ArgumentNullException.ThrowIfNull(keys);
if (keys.Count == 0)
{
return null;
}
return GetUserData(userId, keys[0]);
}
/// <summary>
/// Return all user-data associated with the given user.
/// </summary>
/// <param name="userId">The internal user id.</param>
/// <returns>The list of user item data.</returns>
public List<UserItemData> GetAllUserData(long userId)
{
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
var list = new List<UserItemData>();
using (var connection = GetConnection())
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{
statement.TryBind("@UserId", userId);
foreach (var row in statement.ExecuteQuery())
{
list.Add(ReadRow(row));
}
}
}
return list;
}
/// <summary>
/// Read a row from the specified reader into the provided userData object.
/// </summary>
/// <param name="reader">The list of result set values.</param>
/// <returns>The user item data.</returns>
private UserItemData ReadRow(SqliteDataReader reader)
{
var userData = new UserItemData
{
Key = reader.GetString(0)
};
if (reader.TryGetDouble(2, out var rating))
{
userData.Rating = rating;
}
userData.Played = reader.GetBoolean(3);
userData.PlayCount = reader.GetInt32(4);
userData.IsFavorite = reader.GetBoolean(5);
userData.PlaybackPositionTicks = reader.GetInt64(6);
if (reader.TryReadDateTime(7, out var lastPlayedDate))
{
userData.LastPlayedDate = lastPlayedDate;
}
if (reader.TryGetInt32(8, out var audioStreamIndex))
{
userData.AudioStreamIndex = audioStreamIndex;
}
if (reader.TryGetInt32(9, out var subtitleStreamIndex))
{
userData.SubtitleStreamIndex = subtitleStreamIndex;
}
return userData;
}
}
}

View File

@@ -0,0 +1,30 @@
namespace Emby.Server.Implementations.Data;
/// <summary>
/// The disk synchronization mode, controls how aggressively SQLite will write data
/// all the way out to physical storage.
/// </summary>
public enum SynchronousMode
{
/// <summary>
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
/// </summary>
Off = 0,
/// <summary>
/// SQLite database engine will still sync at the most critical moments.
/// </summary>
Normal = 1,
/// <summary>
/// SQLite database engine will use the xSync method of the VFS
/// to ensure that all content is safely written to the disk surface prior to continuing.
/// </summary>
Full = 2,
/// <summary>
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
/// </summary>
Extra = 3
}

View File

@@ -0,0 +1,23 @@
namespace Emby.Server.Implementations.Data;
/// <summary>
/// Storage mode used by temporary database files.
/// </summary>
public enum TempStoreMode
{
/// <summary>
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
/// is used to determine where temporary tables and indices are stored.
/// </summary>
Default = 0,
/// <summary>
/// Temporary tables and indices are stored in a file.
/// </summary>
File = 1,
/// <summary>
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
/// </summary>
Memory = 2
}

View File

@@ -4,7 +4,6 @@ using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -14,7 +13,7 @@ namespace Emby.Server.Implementations.Devices
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<DeviceId> _logger; private readonly ILogger<DeviceId> _logger;
private readonly Lock _syncLock = new(); private readonly object _syncLock = new object();
private string? _id; private string? _id;

View File

@@ -1,23 +1,22 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Trickplay; using MediaBrowser.Controller.Trickplay;
@@ -38,80 +37,10 @@ namespace Emby.Server.Implementations.Dto
{ {
public class DtoService : IDtoService public class DtoService : IDtoService
{ {
private static readonly FrozenDictionary<BaseItemKind, BaseItemKind[]> _relatedItemKinds = new Dictionary<BaseItemKind, BaseItemKind[]>
{
{
BaseItemKind.Genre, [
BaseItemKind.Audio,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo,
BaseItemKind.Series,
BaseItemKind.Trailer
]
},
{
BaseItemKind.MusicArtist, [
BaseItemKind.Audio,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicVideo
]
},
{
BaseItemKind.MusicGenre, [
BaseItemKind.Audio,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo
]
},
{
BaseItemKind.Person, [
BaseItemKind.Audio,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo,
BaseItemKind.Series,
BaseItemKind.Trailer
]
},
{
BaseItemKind.Studio, [
BaseItemKind.Audio,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo,
BaseItemKind.Series,
BaseItemKind.Trailer
]
},
{
BaseItemKind.Year, [
BaseItemKind.Audio,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo,
BaseItemKind.Series,
BaseItemKind.Trailer
]
}
}.ToFrozenDictionary();
private readonly ILogger<DtoService> _logger; private readonly ILogger<DtoService> _logger;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataRepository; private readonly IUserDataManager _userDataRepository;
private readonly IItemRepository _itemRepo;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly IProviderManager _providerManager; private readonly IProviderManager _providerManager;
@@ -122,24 +51,24 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory; private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ITrickplayManager _trickplayManager; private readonly ITrickplayManager _trickplayManager;
private readonly IChapterManager _chapterManager;
public DtoService( public DtoService(
ILogger<DtoService> logger, ILogger<DtoService> logger,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IUserDataManager userDataRepository, IUserDataManager userDataRepository,
IItemRepository itemRepo,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
IProviderManager providerManager, IProviderManager providerManager,
IRecordingsManager recordingsManager, IRecordingsManager recordingsManager,
IApplicationHost appHost, IApplicationHost appHost,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory, Lazy<ILiveTvManager> livetvManagerFactory,
ITrickplayManager trickplayManager, ITrickplayManager trickplayManager)
IChapterManager chapterManager)
{ {
_logger = logger; _logger = logger;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userDataRepository = userDataRepository; _userDataRepository = userDataRepository;
_itemRepo = itemRepo;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_providerManager = providerManager; _providerManager = providerManager;
_recordingsManager = recordingsManager; _recordingsManager = recordingsManager;
@@ -147,7 +76,6 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory; _livetvManagerFactory = livetvManagerFactory;
_trickplayManager = trickplayManager; _trickplayManager = trickplayManager;
_chapterManager = chapterManager;
} }
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -167,16 +95,28 @@ namespace Emby.Server.Implementations.Dto
if (item is LiveTvChannel tvChannel) if (item is LiveTvChannel tvChannel)
{ {
(channelTuples ??= []).Add((dto, tvChannel)); (channelTuples ??= new()).Add((dto, tvChannel));
} }
else if (item is LiveTvProgram) else if (item is LiveTvProgram)
{ {
(programTuples ??= []).Add((item, dto)); (programTuples ??= new()).Add((item, dto));
} }
if (options.ContainsField(ItemFields.ItemCounts)) if (item is IItemByName byName)
{ {
SetItemByNameInfo(dto, user); if (options.ContainsField(ItemFields.ItemCounts))
{
var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = new DtoOptions(false)
{
EnableImages = false
}
});
SetItemByNameInfo(item, dto, libraryItems);
}
} }
returnItems[index] = dto; returnItems[index] = dto;
@@ -207,14 +147,34 @@ namespace Emby.Server.Implementations.Dto
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult(); LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
} }
if (options.ContainsField(ItemFields.ItemCounts)) if (item is IItemByName itemByName
&& options.ContainsField(ItemFields.ItemCounts))
{ {
SetItemByNameInfo(dto, user); SetItemByNameInfo(
item,
dto,
GetTaggedItems(
itemByName,
user,
new DtoOptions(false)
{
EnableImages = false
}));
} }
return dto; return dto;
} }
private static IList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
{
return byName.GetTaggedItems(
new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = options
});
}
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null) private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
{ {
var dto = new BaseItemDto var dto = new BaseItemDto
@@ -355,15 +315,11 @@ namespace Emby.Server.Implementations.Dto
} }
/// <inheritdoc /> /// <inheritdoc />
/// TODO refactor this to use the new SetItemByNameInfo.
/// Some callers already have the counts extracted so no reason to retrieve them again.
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null) public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null)
{ {
var dto = GetBaseItemDtoInternal(item, options, user); var dto = GetBaseItemDtoInternal(item, options, user);
if (options.ContainsField(ItemFields.ItemCounts) if (taggedItems is not null && options.ContainsField(ItemFields.ItemCounts))
&& taggedItems is not null
&& taggedItems.Count != 0)
{ {
SetItemByNameInfo(item, dto, taggedItems); SetItemByNameInfo(item, dto, taggedItems);
} }
@@ -371,58 +327,7 @@ namespace Emby.Server.Implementations.Dto
return dto; return dto;
} }
private void SetItemByNameInfo(BaseItemDto dto, User? user) private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
{
if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
{
return;
}
var query = new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = new DtoOptions(false) { EnableImages = false },
IncludeItemTypes = relatedItemKinds
};
switch (dto.Type)
{
case BaseItemKind.Genre:
case BaseItemKind.MusicGenre:
query.GenreIds = [dto.Id];
break;
case BaseItemKind.MusicArtist:
query.ArtistIds = [dto.Id];
break;
case BaseItemKind.Person:
query.PersonIds = [dto.Id];
break;
case BaseItemKind.Studio:
query.StudioIds = [dto.Id];
break;
case BaseItemKind.Year
when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
query.Years = [year];
break;
default:
return;
}
var counts = _libraryManager.GetItemCounts(query);
dto.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount;
dto.EpisodeCount = counts.EpisodeCount;
dto.MovieCount = counts.MovieCount;
dto.MusicVideoCount = counts.MusicVideoCount;
dto.ProgramCount = counts.ProgramCount;
dto.SeriesCount = counts.SeriesCount;
dto.SongCount = counts.SongCount;
dto.TrailerCount = counts.TrailerCount;
dto.ChildCount = counts.TotalItemCount();
}
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
{ {
if (item is MusicArtist) if (item is MusicArtist)
{ {
@@ -681,12 +586,12 @@ namespace Emby.Server.Implementations.Dto
if (dto.ImageBlurHashes is not null) if (dto.ImageBlurHashes is not null)
{ {
// Only add BlurHash for the person's image. // Only add BlurHash for the person's image.
baseItemPerson.ImageBlurHashes = []; baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
foreach (var (imageType, blurHash) in dto.ImageBlurHashes) foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
{ {
if (blurHash is not null) if (blurHash is not null)
{ {
baseItemPerson.ImageBlurHashes[imageType] = []; baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
foreach (var (imageId, blurHashValue) in blurHash) foreach (var (imageId, blurHashValue) in blurHash)
{ {
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase)) if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
@@ -765,11 +670,11 @@ namespace Emby.Server.Implementations.Dto
if (!string.IsNullOrEmpty(image.BlurHash)) if (!string.IsNullOrEmpty(image.BlurHash))
{ {
dto.ImageBlurHashes ??= []; dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value)) if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
{ {
value = []; value = new Dictionary<string, string>();
dto.ImageBlurHashes[image.Type] = value; dto.ImageBlurHashes[image.Type] = value;
} }
@@ -800,7 +705,7 @@ namespace Emby.Server.Implementations.Dto
if (hashes.Count > 0) if (hashes.Count > 0)
{ {
dto.ImageBlurHashes ??= []; dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
dto.ImageBlurHashes[imageType] = hashes; dto.ImageBlurHashes[imageType] = hashes;
} }
@@ -847,7 +752,7 @@ namespace Emby.Server.Implementations.Dto
dto.AspectRatio = hasAspectRatio.AspectRatio; dto.AspectRatio = hasAspectRatio.AspectRatio;
} }
dto.ImageBlurHashes = []; dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
var backdropLimit = options.GetImageLimit(ImageType.Backdrop); var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
if (backdropLimit > 0) if (backdropLimit > 0)
@@ -863,7 +768,7 @@ namespace Emby.Server.Implementations.Dto
if (options.EnableImages) if (options.EnableImages)
{ {
dto.ImageTags = []; dto.ImageTags = new Dictionary<ImageType, string>();
// Prevent implicitly captured closure // Prevent implicitly captured closure
var currentItem = item; var currentItem = item;
@@ -1051,15 +956,30 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor // Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]) dto.ArtistItems = hasArtist.Artists
.Where(e => e.Value.Length > 0) // .Except(foundArtists, new DistinctNameComparer())
.Select(i => .Select(i =>
{ {
return new NameGuidPair // This should not be necessary but we're seeing some cases of it
if (string.IsNullOrEmpty(i))
{ {
Name = i.Key, return null;
Id = i.Value.First().Id }
};
var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
{
EnableImages = false
});
if (artist is not null)
{
return new NameGuidPair
{
Name = artist.Name,
Id = artist.Id
};
}
return null;
}).Where(i => i is not null).ToArray(); }).Where(i => i is not null).ToArray();
} }
@@ -1140,17 +1060,12 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Chapters)) if (options.ContainsField(ItemFields.Chapters))
{ {
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList(); dto.Chapters = _itemRepo.GetChapters(item);
} }
if (options.ContainsField(ItemFields.Trickplay)) if (options.ContainsField(ItemFields.Trickplay))
{ {
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
dto.Trickplay = trickplay.ToDictionary(
mediaStream => mediaStream.Key,
mediaStream => mediaStream.Value.ToDictionary(
width => width.Key,
width => new TrickplayInfoDto(width.Value)));
} }
dto.ExtraType = video.ExtraType; dto.ExtraType = video.ExtraType;

View File

@@ -18,11 +18,9 @@
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" /> <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" /> <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" /> <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
<ProjectReference Include="..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BitFaster.Caching" />
<PackageReference Include="DiscUtils.Udf" /> <PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
@@ -39,7 +37,7 @@
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
@@ -64,14 +62,10 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Ignore" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Localization\iso6392.txt" /> <EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" /> <EmbeddedResource Include="Localization\countries.json" />
<EmbeddedResource Include="Localization\Core\*.json" /> <EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.json" /> <EmbeddedResource Include="Localization\Ratings\*.csv" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -5,8 +5,8 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@@ -34,7 +34,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly ILogger<LibraryChangedNotifier> _logger; private readonly ILogger<LibraryChangedNotifier> _logger;
private readonly Lock _libraryChangedSyncLock = new(); private readonly object _libraryChangedSyncLock = new();
private readonly List<Folder> _foldersAddedTo = new(); private readonly List<Folder> _foldersAddedTo = new();
private readonly List<Folder> _foldersRemovedFrom = new(); private readonly List<Folder> _foldersRemovedFrom = new();
private readonly List<BaseItem> _itemsAdded = new(); private readonly List<BaseItem> _itemsAdded = new();

View File

@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new(); private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
private readonly Lock _syncLock = new(); private readonly object _syncLock = new();
private Timer? _updateTimer; private Timer? _updateTimer;
@@ -144,15 +144,9 @@ namespace Emby.Server.Implementations.EntryPoints
.Select(i => .Select(i =>
{ {
var dto = _userDataManager.GetUserDataDto(i, user); var dto = _userDataManager.GetUserDataDto(i, user);
if (dto is null)
{
return null!;
}
dto.ItemId = i.Id; dto.ItemId = i.Id;
return dto; return dto;
}) })
.Where(e => e is not null)
.ToArray() .ToArray()
}; };
} }

View File

@@ -1,8 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;

View File

@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint; RemoteEndPoint = remoteEndPoint;
_jsonOptions = JsonDefaults.Options; _jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.UtcNow; LastActivityDate = DateTime.Now;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -82,17 +82,17 @@ namespace Emby.Server.Implementations.HttpServer
public WebSocketState State => _socket.State; public WebSocketState State => _socket.State;
/// <inheritdoc /> /// <inheritdoc />
public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken) public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
{ {
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false); return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken) public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
{ {
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false); return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -224,12 +224,12 @@ namespace Emby.Server.Implementations.HttpServer
return ret; return ret;
} }
private async Task SendKeepAliveResponse() private Task SendKeepAliveResponse()
{ {
LastKeepAliveDate = DateTime.UtcNow; LastKeepAliveDate = DateTime.UtcNow;
await SendAsync( return SendAsync(
new OutboundKeepAliveMessage(), new OutboundKeepAliveMessage(),
CancellationToken.None).ConfigureAwait(false); CancellationToken.None);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.HttpServer
/// Processes the web socket message received. /// Processes the web socket message received.
/// </summary> /// </summary>
/// <param name="result">The result.</param> /// <param name="result">The result.</param>
private async Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
{ {
var tasks = new Task[_webSocketListeners.Length]; var tasks = new Task[_webSocketListeners.Length];
for (var i = 0; i < _webSocketListeners.Length; ++i) for (var i = 0; i < _webSocketListeners.Length; ++i)
@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.HttpServer
tasks[i] = _webSocketListeners[i].ProcessMessageAsync(result); tasks[i] = _webSocketListeners[i].ProcessMessageAsync(result);
} }
await Task.WhenAll(tasks).ConfigureAwait(false); return Task.WhenAll(tasks);
} }
} }
} }

View File

@@ -18,8 +18,8 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;
private readonly List<string> _affectedPaths = new(); private readonly List<string> _affectedPaths = new List<string>();
private readonly Lock _timerLock = new(); private readonly object _timerLock = new object();
private Timer? _timer; private Timer? _timer;
private bool _disposed; private bool _disposed;
@@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.IO
private void ProcessPathChanges(List<string> paths) private void ProcessPathChanges(List<string> paths)
{ {
IEnumerable<BaseItem> itemsToRefresh = paths IEnumerable<BaseItem> itemsToRefresh = paths
.Distinct() .Distinct(StringComparer.OrdinalIgnoreCase)
.Select(GetAffectedBaseItem) .Select(GetAffectedBaseItem)
.Where(item => item is not null) .Where(item => item is not null)
.DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where() .DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where()

View File

@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.IO
.Where(IsLibraryMonitorEnabled) .Where(IsLibraryMonitorEnabled)
.OfType<Folder>() .OfType<Folder>()
.SelectMany(f => f.PhysicalLocations) .SelectMany(f => f.PhysicalLocations)
.Distinct() .Distinct(StringComparer.OrdinalIgnoreCase)
.Order(); .Order();
foreach (var path in paths) foreach (var path in paths)

View File

@@ -6,7 +6,6 @@ using System.Linq;
using System.Security; using System.Security;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -153,10 +152,6 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc /> /// <inheritdoc />
public void MoveDirectory(string source, string destination) public void MoveDirectory(string source, string destination)
{ {
// Make sure parent directory of target exists
var parent = Directory.GetParent(destination);
parent?.Create();
try try
{ {
Directory.Move(source, destination); Directory.Move(source, destination);
@@ -165,13 +160,12 @@ namespace Emby.Server.Implementations.IO
{ {
// Cross device move requires a copy // Cross device move requires a copy
Directory.CreateDirectory(destination); Directory.CreateDirectory(destination);
var sourceDir = new DirectoryInfo(source); foreach (string file in Directory.GetFiles(source))
foreach (var file in sourceDir.EnumerateFiles())
{ {
file.CopyTo(Path.Combine(destination, file.Name), true); File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true);
} }
sourceDir.Delete(true); Directory.Delete(source, true);
} }
} }
@@ -253,40 +247,47 @@ namespace Emby.Server.Implementations.IO
{ {
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory; result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory)
// {
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
// }
if (info is FileInfo fileInfo) if (info is FileInfo fileInfo)
{ {
result.CreationTimeUtc = GetCreationTimeUtc(info); result.Length = fileInfo.Length;
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
if (fileInfo.LinkTarget is not null) // Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{ {
try try
{ {
var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true); using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
if (targetFileInfo is not null)
{ {
result.Exists = targetFileInfo.Exists; result.Length = RandomAccess.GetLength(fileHandle);
if (result.Exists)
{
result.Length = targetFileInfo.Length;
result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
}
}
else
{
result.Exists = false;
} }
} }
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
catch (UnauthorizedAccessException ex) catch (UnauthorizedAccessException ex)
{ {
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName); _logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
} }
} catch (IOException ex)
else {
{ // IOException generally means the file is not accessible due to filesystem issues
result.Length = fileInfo.Length; // Catch this exception and mark the file as not exist to ignore it
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
result.Exists = false;
}
} }
} }
result.CreationTimeUtc = GetCreationTimeUtc(info);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
} }
else else
{ {
@@ -540,8 +541,8 @@ namespace Emby.Server.Implementations.IO
return DriveInfo.GetDrives() return DriveInfo.GetDrives()
.Where( .Where(
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable) d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
&& d.IsReady && d.IsReady
&& d.TotalSize != 0) && d.TotalSize != 0)
.Select(d => new FileSystemMetadata .Select(d => new FileSystemMetadata
{ {
Name = d.Name, Name = d.Name,
@@ -559,36 +560,22 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc /> /// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
{ {
return GetFiles(path, "*", recursive); return GetFiles(path, null, false, recursive);
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
return GetFiles(path, searchPattern, null, false, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive)
{
return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{ {
var enumerationOptions = GetEnumerationOptions(recursive); var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and macOS the search pattern is case-sensitive // On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1) if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
{ {
searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0]; return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions));
} }
var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions); var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
if (extensions is not null && extensions.Count > 0) if (extensions is not null && extensions.Count > 0)
{ {
@@ -641,7 +628,7 @@ namespace Emby.Server.Implementations.IO
{ {
var enumerationOptions = GetEnumerationOptions(recursive); var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and macOS the search pattern is case-sensitive // On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1) if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1)
{ {

View File

@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@@ -43,11 +42,13 @@ namespace Emby.Server.Implementations.Images
protected IImageProcessor ImageProcessor { get; set; } protected IImageProcessor ImageProcessor { get; set; }
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; } protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
= [ImageType.Primary]; = new ImageType[] { ImageType.Primary };
/// <inheritdoc /> /// <inheritdoc />
public string Name => "Dynamic Image Provider"; public string Name => "Dynamic Image Provider";
protected virtual int MaxImageAgeDays => 7;
public int Order => 0; public int Order => 0;
protected virtual bool Supports(BaseItem item) => true; protected virtual bool Supports(BaseItem item) => true;
@@ -115,9 +116,9 @@ namespace Emby.Server.Implementations.Images
var mimeType = MimeTypes.GetMimeType(outputPath); var mimeType = MimeTypes.GetMimeType(outputPath);
if (string.Equals(mimeType, MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase)) if (string.Equals(mimeType, "application/octet-stream", StringComparison.OrdinalIgnoreCase))
{ {
mimeType = MediaTypeNames.Image.Png; mimeType = "image/png";
} }
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false); await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
@@ -290,14 +291,8 @@ namespace Emby.Server.Implementations.Images
protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image) protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{ {
var path = image.Path; var age = DateTime.UtcNow - image.DateModified;
if (!string.IsNullOrEmpty(path)) return age.TotalDays > MaxImageAgeDays;
{
var modificationDate = FileSystem.GetLastWriteTimeUtc(path);
return image.DateModified != modificationDate;
}
return false;
} }
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType) protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;

View File

@@ -6,7 +6,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;

View File

@@ -4,7 +4,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;

View File

@@ -37,33 +37,46 @@ namespace Emby.Server.Implementations.Library
return false; return false;
} }
// Don't ignore top level folders
if (fileInfo.IsDirectory && parent is AggregateFolder)
{
return false;
}
if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
{ {
return true; return true;
} }
// Don't ignore top level folders var filename = fileInfo.Name;
if (fileInfo.IsDirectory
&& (parent is AggregateFolder || (parent?.IsTopParent ?? false)))
{
return false;
}
if (parent is null)
{
return false;
}
if (fileInfo.IsDirectory) if (fileInfo.IsDirectory)
{ {
// Ignore extras for unsupported types if (parent is not null)
return _namingOptions.AllExtrasTypesFolderNames.ContainsKey(fileInfo.Name) {
&& parent is not UserRootFolder; // Ignore extras folders but allow it at the collection level
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
&& parent is not AggregateFolder
&& parent is not UserRootFolder)
{
return true;
}
}
}
else
{
if (parent is not null)
{
// Don't resolve these into audio files
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
{
return true;
}
}
} }
// Don't resolve theme songs return false;
return Path.GetFileNameWithoutExtension(fileInfo.Name.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& AudioFileParser.IsAudioFile(fileInfo.Name, _namingOptions);
} }
} }
} }

View File

@@ -1,96 +0,0 @@
using System;
using System.IO;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// Resolver rule class for ignoring files via .ignore.
/// </summary>
public class DotIgnoreIgnoreRule : IResolverIgnoreRule
{
private static readonly bool IsWindows = OperatingSystem.IsWindows();
private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
{
for (var current = directory; current is not null; current = current.Parent)
{
var ignorePath = Path.Join(current.FullName, ".ignore");
if (File.Exists(ignorePath))
{
return new FileInfo(ignorePath);
}
}
return null;
}
/// <inheritdoc />
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
/// <summary>
/// Checks whether or not the file is ignored.
/// </summary>
/// <param name="fileInfo">The file information.</param>
/// <param name="parent">The parent BaseItem.</param>
/// <returns>True if the file should be ignored.</returns>
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
{
var searchDirectory = fileInfo.IsDirectory
? new DirectoryInfo(fileInfo.FullName)
: new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
if (string.IsNullOrEmpty(searchDirectory.FullName))
{
return false;
}
var ignoreFile = FindIgnoreFile(searchDirectory);
if (ignoreFile is null)
{
return false;
}
// Fast path in case the ignore files isn't a symlink and is empty
if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
{
// Ignore directory if we just have the file
return true;
}
var content = GetFileContent(ignoreFile);
return string.IsNullOrWhiteSpace(content)
|| CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
}
private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
{
// If file has content, base ignoring off the content .gitignore-style rules
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var ignore = new Ignore.Ignore();
ignore.Add(rules);
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
// See https://github.com/jellyfin/jellyfin/issues/15484
var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
// Add trailing slash for directories to match "folder/"
if (isDirectory)
{
pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
}
return ignore.IsIgnored(pathToCheck);
}
private static string GetFileContent(FileInfo ignoreFile)
{
ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
return ignoreFile.Exists
? File.ReadAllText(ignoreFile.FullName)
: string.Empty;
}
}

View File

@@ -1,77 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Trickplay;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// IExternalDataManager implementation.
/// </summary>
public class ExternalDataManager : IExternalDataManager
{
private readonly IKeyframeManager _keyframeManager;
private readonly IMediaSegmentManager _mediaSegmentManager;
private readonly IPathManager _pathManager;
private readonly ITrickplayManager _trickplayManager;
private readonly IChapterManager _chapterManager;
private readonly ILogger<ExternalDataManager> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ExternalDataManager"/> class.
/// </summary>
/// <param name="keyframeManager">The keyframe manager.</param>
/// <param name="mediaSegmentManager">The media segment manager.</param>
/// <param name="pathManager">The path manager.</param>
/// <param name="trickplayManager">The trickplay manager.</param>
/// <param name="chapterManager">The chapter manager.</param>
/// <param name="logger">The logger.</param>
public ExternalDataManager(
IKeyframeManager keyframeManager,
IMediaSegmentManager mediaSegmentManager,
IPathManager pathManager,
ITrickplayManager trickplayManager,
IChapterManager chapterManager,
ILogger<ExternalDataManager> logger)
{
_keyframeManager = keyframeManager;
_mediaSegmentManager = mediaSegmentManager;
_pathManager = pathManager;
_trickplayManager = trickplayManager;
_chapterManager = chapterManager;
_logger = logger;
}
/// <inheritdoc/>
public async Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken)
{
var validPaths = _pathManager.GetExtractedDataPaths(item).Where(Directory.Exists).ToList();
var itemId = item.Id;
if (validPaths.Count > 0)
{
foreach (var path in validPaths)
{
try
{
Directory.Delete(path, true);
}
catch (Exception ex)
{
_logger.LogWarning("Unable to prune external item data at {Path}: {Exception}", path, ex);
}
}
}
await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
await _chapterManager.DeleteChapterDataAsync(itemId, cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using DotNet.Globbing; using DotNet.Globbing;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
@@ -48,8 +49,6 @@ namespace Emby.Server.Implementations.Library
"**/.wd_tv", "**/.wd_tv",
"**/lost+found/**", "**/lost+found/**",
"**/lost+found", "**/lost+found",
"**/subs/**",
"**/subs",
// Trickplay files // Trickplay files
"**/*.trickplay", "**/*.trickplay",

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.MediaEncoding.Keyframes;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Persistence;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// Manager for Keyframe data.
/// </summary>
public class KeyframeManager : IKeyframeManager
{
private readonly IKeyframeRepository _repository;
/// <summary>
/// Initializes a new instance of the <see cref="KeyframeManager"/> class.
/// </summary>
/// <param name="repository">The keyframe repository.</param>
public KeyframeManager(IKeyframeRepository repository)
{
_repository = repository;
}
/// <inheritdoc />
public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId)
{
return _repository.GetKeyframeData(itemId);
}
/// <inheritdoc />
public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken)
{
await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
{
await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -13,10 +12,8 @@ using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncKeyedLock; using AsyncKeyedLock;
using Jellyfin.Data; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@@ -41,7 +38,7 @@ namespace Emby.Server.Implementations.Library
public class MediaSourceManager : IMediaSourceManager, IDisposable public class MediaSourceManager : IMediaSourceManager, IDisposable
{ {
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char LiveStreamIdDelimiter = '_'; private const char LiveStreamIdDelimeter = '_';
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IItemRepository _itemRepo; private readonly IItemRepository _itemRepo;
@@ -54,8 +51,7 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager; private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly IMediaStreamRepository _mediaStreamRepository;
private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1); private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -73,9 +69,7 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem, IFileSystem fileSystem,
IUserDataManager userDataManager, IUserDataManager userDataManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IDirectoryService directoryService, IDirectoryService directoryService)
IMediaStreamRepository mediaStreamRepository,
IMediaAttachmentRepository mediaAttachmentRepository)
{ {
_appHost = appHost; _appHost = appHost;
_itemRepo = itemRepo; _itemRepo = itemRepo;
@@ -88,8 +82,6 @@ namespace Emby.Server.Implementations.Library
_localizationManager = localizationManager; _localizationManager = localizationManager;
_appPaths = applicationPaths; _appPaths = applicationPaths;
_directoryService = directoryService; _directoryService = directoryService;
_mediaStreamRepository = mediaStreamRepository;
_mediaAttachmentRepository = mediaAttachmentRepository;
} }
public void AddParts(IEnumerable<IMediaSourceProvider> providers) public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -97,9 +89,9 @@ namespace Emby.Server.Implementations.Library
_providers = providers.ToArray(); _providers = providers.ToArray();
} }
public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query) public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
{ {
var list = _mediaStreamRepository.GetMediaStreams(query); var list = _itemRepo.GetMediaStreams(query);
foreach (var stream in list) foreach (var stream in list)
{ {
@@ -129,7 +121,7 @@ namespace Emby.Server.Implementations.Library
return false; return false;
} }
public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId) public List<MediaStream> GetMediaStreams(Guid itemId)
{ {
var list = GetMediaStreams(new MediaStreamQuery var list = GetMediaStreams(new MediaStreamQuery
{ {
@@ -139,7 +131,7 @@ namespace Emby.Server.Implementations.Library
return GetMediaStreamsForItem(list); return GetMediaStreamsForItem(list);
} }
private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams) private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams)
{ {
foreach (var stream in streams) foreach (var stream in streams)
{ {
@@ -153,13 +145,13 @@ namespace Emby.Server.Implementations.Library
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query) public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
{ {
return _mediaAttachmentRepository.GetMediaAttachments(query); return _itemRepo.GetMediaAttachments(query);
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId) public List<MediaAttachment> GetMediaAttachments(Guid itemId)
{ {
return GetMediaAttachments(new MediaAttachmentQuery return GetMediaAttachments(new MediaAttachmentQuery
{ {
@@ -167,7 +159,7 @@ namespace Emby.Server.Implementations.Library
}); });
} }
public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{ {
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -220,17 +212,12 @@ namespace Emby.Server.Implementations.Library
list.Add(source); list.Add(source);
} }
return SortMediaSources(list).ToArray(); return SortMediaSources(list);
} }
/// <inheritdoc />> /// <inheritdoc />>
public MediaProtocol GetPathProtocol(string path) public MediaProtocol GetPathProtocol(string path)
{ {
if (string.IsNullOrEmpty(path))
{
return MediaProtocol.File;
}
if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
{ {
return MediaProtocol.Rtsp; return MediaProtocol.Rtsp;
@@ -320,7 +307,7 @@ namespace Emby.Server.Implementations.Library
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource) private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
{ {
var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter; var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter;
if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{ {
@@ -345,7 +332,7 @@ namespace Emby.Server.Implementations.Library
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
} }
public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null) public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
{ {
ArgumentNullException.ThrowIfNull(item); ArgumentNullException.ThrowIfNull(item);
@@ -384,7 +371,7 @@ namespace Emby.Server.Implementations.Library
var culture = _localizationManager.FindLanguageInfo(language); var culture = _localizationManager.FindLanguageInfo(language);
if (culture is not null) if (culture is not null)
{ {
return culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) ? [culture.Name] : culture.ThreeLetterISOLanguageNames; return culture.ThreeLetterISOLanguageNames;
} }
return [language]; return [language];
@@ -432,7 +419,6 @@ namespace Emby.Server.Implementations.Library
if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index)) if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
{ {
source.DefaultAudioStreamIndex = index; source.DefaultAudioStreamIndex = index;
source.DefaultAudioIndexSource = AudioIndexSource.User;
return; return;
} }
} }
@@ -440,15 +426,6 @@ namespace Emby.Server.Implementations.Library
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference); var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
if (user.PlayDefaultAudioTrack)
{
source.DefaultAudioIndexSource |= AudioIndexSource.Default;
}
if (preferredAudio.Count > 0)
{
source.DefaultAudioIndexSource |= AudioIndexSource.Language;
}
} }
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user) public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
@@ -476,7 +453,7 @@ namespace Emby.Server.Implementations.Library
} }
} }
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources) private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{ {
return sources.OrderBy(i => return sources.OrderBy(i =>
{ {
@@ -493,7 +470,8 @@ namespace Emby.Server.Implementations.Library
return stream?.Width ?? 0; return stream?.Width ?? 0;
}) })
.Where(i => i.Type != MediaSourceType.Placeholder); .Where(i => i.Type != MediaSourceType.Placeholder)
.ToList();
} }
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken) public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -662,7 +640,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogDebug(ex, "Error parsing cached media info."); _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
} }
finally finally
{ {
@@ -686,17 +664,17 @@ namespace Emby.Server.Implementations.Library
mediaInfo = await _mediaEncoder.GetMediaInfo( mediaInfo = await _mediaEncoder.GetMediaInfo(
new MediaInfoRequest new MediaInfoRequest
{ {
MediaSource = mediaSource, MediaSource = mediaSource,
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
ExtractChapters = false ExtractChapters = false
}, },
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
if (cacheFilePath is not null) if (cacheFilePath is not null)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
FileStream createStream = AsyncFile.Create(cacheFilePath); FileStream createStream = File.Create(cacheFilePath);
await using (createStream.ConfigureAwait(false)) await using (createStream.ConfigureAwait(false))
{ {
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -799,13 +777,9 @@ namespace Emby.Server.Implementations.Library
{ {
ArgumentException.ThrowIfNullOrEmpty(id); ArgumentException.ThrowIfNullOrEmpty(id);
var info = GetLiveStreamInfo(id); // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
if (info is null) var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
{ return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null));
}
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
} }
public ILiveStream GetLiveStreamInfo(string id) public ILiveStream GetLiveStreamInfo(string id)
@@ -832,7 +806,7 @@ namespace Emby.Server.Implementations.Library
return result.Item1; return result.Item1;
} }
public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken) public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
{ {
var stream = new MediaSourceInfo var stream = new MediaSourceInfo
{ {
@@ -855,7 +829,10 @@ namespace Emby.Server.Implementations.Library
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths) await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false); .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
return [stream]; return new List<MediaSourceInfo>
{
stream
};
} }
public async Task CloseLiveStream(string id) public async Task CloseLiveStream(string id)
@@ -887,11 +864,11 @@ namespace Emby.Server.Implementations.Library
{ {
ArgumentException.ThrowIfNullOrEmpty(key); ArgumentException.ThrowIfNullOrEmpty(key);
var keys = key.Split(LiveStreamIdDelimiter, 2); var keys = key.Split(LiveStreamIdDelimeter, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase)); var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal); var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1); var keyId = key.Substring(splitIndex + 1);
return (provider, keyId); return (provider, keyId);

View File

@@ -3,7 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Jellyfin.Database.Implementations.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@@ -39,48 +39,46 @@ namespace Emby.Server.Implementations.Library
return null; return null;
} }
// Sort in the following order: Default > No tag > Forced
var sortedStreams = streams var sortedStreams = streams
.Where(i => i.Type == MediaStreamType.Subtitle) .Where(i => i.Type == MediaStreamType.Subtitle)
.OrderByDescending(x => x.IsExternal) .OrderByDescending(x => x.IsExternal)
.ThenByDescending(x => x.IsDefault) .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
.ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
.ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
.ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language))
.ThenByDescending(x => x.IsForced) .ThenByDescending(x => x.IsForced)
.ThenByDescending(x => x.IsDefault)
.ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase))
.ToList(); .ToList();
MediaStream? stream = null; MediaStream? stream = null;
if (mode == SubtitlePlaybackMode.Default) if (mode == SubtitlePlaybackMode.Default)
{ {
// Load subtitles according to external, default and forced flags. // Load subtitles according to external, forced and default flags.
stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced); stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
} }
else if (mode == SubtitlePlaybackMode.Smart) else if (mode == SubtitlePlaybackMode.Smart)
{ {
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages. // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
// If no subtitles of preferred language available, use none. // If no subtitles of preferred language available, use default behaviour.
// If the audio language is one of the user's preferred subtitle languages behave like OnlyForced.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{ {
stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages)); stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
} }
else else
{ {
stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); // Respect forced flag.
stream = sortedStreams.FirstOrDefault(x => x.IsForced);
} }
} }
else if (mode == SubtitlePlaybackMode.Always) else if (mode == SubtitlePlaybackMode.Always)
{ {
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour. // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour.
stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ?? stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
} }
else if (mode == SubtitlePlaybackMode.OnlyForced) else if (mode == SubtitlePlaybackMode.OnlyForced)
{ {
// Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language // Only load subtitles that are flagged forced.
stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); stream = sortedStreams.FirstOrDefault(x => x.IsForced);
} }
return stream?.Index; return stream?.Index;
@@ -112,72 +110,40 @@ namespace Emby.Server.Implementations.Library
if (mode == SubtitlePlaybackMode.Default) if (mode == SubtitlePlaybackMode.Default)
{ {
// Prefer embedded metadata over smart logic // Prefer embedded metadata over smart logic
// Load subtitles according to external, default, and forced flags. filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault)
filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced)
.ToList(); .ToList();
} }
else if (mode == SubtitlePlaybackMode.Smart) else if (mode == SubtitlePlaybackMode.Smart)
{ {
// Prefer smart logic over embedded metadata // Prefer smart logic over embedded metadata
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{ {
filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages)) filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
.ToList(); .ToList();
} }
else
{
filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
}
} }
else if (mode == SubtitlePlaybackMode.Always) else if (mode == SubtitlePlaybackMode.Always)
{ {
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior. // Always load the most suitable full subtitles
filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages)) filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
.ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
} }
else if (mode == SubtitlePlaybackMode.OnlyForced) else if (mode == SubtitlePlaybackMode.OnlyForced)
{ {
// Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language // Always load the most suitable full subtitles
filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages); filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
} }
// If filteredStreams is null, initialize it as an empty list to avoid null reference errors // Load forced subs if we have found no suitable full subtitles
filteredStreams ??= new List<MediaStream>(); var iterStreams = filteredStreams is null || filteredStreams.Count == 0
? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
: filteredStreams;
foreach (var stream in filteredStreams) foreach (var stream in iterStreams)
{ {
stream.Score = GetStreamScore(stream, preferredLanguages); stream.Score = GetStreamScore(stream, preferredLanguages);
} }
} }
private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages)
{
// If preferredLanguages is empty, treat it as "any language" (wildcard)
return preferredLanguages.Count == 0 ||
preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase);
}
private static bool IsLanguageUndefined(string language)
{
// Check for null, empty, or known placeholders
return string.IsNullOrEmpty(language) ||
language.Equals("und", StringComparison.OrdinalIgnoreCase) ||
language.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) ||
language.Equals("mul", StringComparison.OrdinalIgnoreCase) ||
language.Equals("zxx", StringComparison.OrdinalIgnoreCase);
}
private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages)
{
return sortedStreams
.Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language)))
.OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
.ThenByDescending(s => IsLanguageUndefined(s.Language))
.ToList();
}
internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences) internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
{ {
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase)); var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));

View File

@@ -2,11 +2,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@@ -26,35 +24,37 @@ namespace Emby.Server.Implementations.Library
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{ {
var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions); var list = new List<BaseItem>
{
item
};
return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))]; list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
return list;
} }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
{ {
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions); return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
} }
public IReadOnlyList<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
{ {
return GetInstantMixFromGenres(item.Genres, user, dtoOptions); return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
} }
public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
{ {
var genres = item var genres = item
.GetRecursiveChildren( .GetRecursiveChildren(user, new InternalItemsQuery(user)
user, {
new InternalItemsQuery(user) IncludeItemTypes = [BaseItemKind.Audio],
{ DtoOptions = dtoOptions
IncludeItemTypes = [BaseItemKind.Audio], })
DtoOptions = dtoOptions
},
out _)
.Cast<Audio>() .Cast<Audio>()
.SelectMany(i => i.Genres) .SelectMany(i => i.Genres)
.Concat(item.Genres) .Concat(item.Genres)
@@ -63,12 +63,12 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenres(genres, user, dtoOptions); return GetInstantMixFromGenres(genres, user, dtoOptions);
} }
public IReadOnlyList<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
{ {
return GetInstantMixFromGenres(item.Genres, user, dtoOptions); return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
} }
public IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
{ {
var genreIds = genres.DistinctNames().Select(i => var genreIds = genres.DistinctNames().Select(i =>
{ {
@@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
} }
public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
{ {
return _libraryManager.GetItemList(new InternalItemsQuery(user) return _libraryManager.GetItemList(new InternalItemsQuery(user)
{ {
@@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Library
}); });
} }
public IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions) public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
{ {
if (item is MusicGenre) if (item is MusicGenre)
{ {

View File

@@ -1,101 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// IPathManager implementation.
/// </summary>
public class PathManager : IPathManager
{
private readonly IServerConfigurationManager _config;
private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="PathManager"/> class.
/// </summary>
/// <param name="config">The server configuration manager.</param>
/// <param name="appPaths">The application paths.</param>
public PathManager(
IServerConfigurationManager config,
IApplicationPaths appPaths)
{
_config = config;
_appPaths = appPaths;
}
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
/// <inheritdoc />
public string GetAttachmentPath(string mediaSourceId, string fileName)
{
return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
}
/// <inheritdoc />
public string GetAttachmentFolderPath(string mediaSourceId)
{
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(AttachmentCachePath, id[..2], id);
}
/// <inheritdoc />
public string GetSubtitleFolderPath(string mediaSourceId)
{
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(SubtitleCachePath, id[..2], id);
}
/// <inheritdoc />
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
{
return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
}
/// <inheritdoc />
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
{
var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return saveWithMedia
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(Path.GetFileName(item.Path), ".trickplay"))
: Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id);
}
/// <inheritdoc/>
public string GetChapterImageFolderPath(BaseItem item)
{
return Path.Combine(item.GetInternalMetadataPath(), "chapters");
}
/// <inheritdoc/>
public string GetChapterImagePath(BaseItem item, long chapterPositionTicks)
{
var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
return Path.Combine(GetChapterImageFolderPath(item), filename);
}
/// <inheritdoc/>
public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
{
var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
return [
GetAttachmentFolderPath(mediaSourceId),
GetSubtitleFolderPath(mediaSourceId),
GetTrickplayDirectory(item, false),
GetTrickplayDirectory(item, true),
GetChapterImageFolderPath(item)
];
}
}

View File

@@ -136,33 +136,23 @@ namespace Emby.Server.Implementations.Library
if (config.UseFileCreationTimeForDateAdded) if (config.UseFileCreationTimeForDateAdded)
{ {
var fileCreationDate = info?.CreationTimeUtc; // directoryService.getFile may return null
if (fileCreationDate is not null) if (info is not null)
{ {
var dateCreated = fileCreationDate; var dateCreated = info.CreationTimeUtc;
if (dateCreated == DateTime.MinValue)
if (dateCreated.Equals(DateTime.MinValue))
{ {
dateCreated = DateTime.UtcNow; dateCreated = DateTime.UtcNow;
} }
item.DateCreated = dateCreated.Value; item.DateCreated = dateCreated;
} }
} }
else else
{ {
item.DateCreated = DateTime.UtcNow; item.DateCreated = DateTime.UtcNow;
} }
if (info is not null && !info.IsDirectory)
{
item.Size = info.Length;
}
var fileModificationDate = info?.LastWriteTimeUtc;
if (fileModificationDate.HasValue)
{
item.DateModified = fileModificationDate.Value;
}
} }
} }
} }

View File

@@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_ => _videoResolvers _ => _videoResolvers
}; };
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "") public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
{ {
var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot); var extraResult = GetExtraInfo(path, _namingOptions);
if (extraResult.ExtraType is null) if (extraResult.ExtraType is null)
{ {
extraType = null; extraType = null;

View File

@@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
} }
var videoInfos = files var videoInfos = files
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath)) .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
.Where(f => f is not null) .Where(f => f is not null)
.ToList(); .ToList();
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath); var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
var result = new MultiItemResolverResult var result = new MultiItemResolverResult
{ {
@@ -369,16 +369,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// We need to only look at the name of this actual item (not parents) // We need to only look at the name of this actual item (not parents)
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan()); var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
var tmdbid = justName.GetAttributeValue("tmdbid"); if (!justName.IsEmpty)
// If not in a mixed folder and ID not found in folder path, check filename
if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
{ {
tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid"); // Check for TMDb id
var tmdbid = justName.GetAttributeValue("tmdbid");
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
} }
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
if (!string.IsNullOrEmpty(item.Path)) if (!string.IsNullOrEmpty(item.Path))
{ {
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name) // Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
@@ -408,11 +405,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (child.IsDirectory) if (child.IsDirectory)
{ {
if (NamingOptions.AllExtrasTypesFolderNames.ContainsKey(filename))
{
continue;
}
if (IsDvdDirectory(child.FullName, filename, directoryService)) if (IsDvdDirectory(child.FullName, filename, directoryService))
{ {
var movie = new T var movie = new T
@@ -464,17 +456,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{ {
var videoPath = result.Items[0].Path; var videoPath = result.Items[0].Path;
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name)); var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
var hasOtherSubfolders = multiDiscFolders.Count > 0;
if (!hasPhotos && !hasOtherSubfolders) if (!hasPhotos)
{ {
var movie = (T)result.Items[0]; var movie = (T)result.Items[0];
movie.IsInMixedFolder = false; movie.IsInMixedFolder = false;
if (collectionType == CollectionType.movies || collectionType is null) movie.Name = Path.GetFileName(movie.ContainingFolderPath);
{
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
}
return movie; return movie;
} }
} }

View File

@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path; var path = args.Path;
var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true); var seasonParserResult = SeasonPathParser.Parse(path, true, true);
var season = new Season var season = new Season
{ {

View File

@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
if (child.IsDirectory) if (child.IsDirectory)
{ {
if (IsSeasonFolder(child.FullName, path, isTvContentType)) if (IsSeasonFolder(child.FullName, isTvContentType))
{ {
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true; return true;
@@ -155,12 +155,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// Determines whether [is season folder] [the specified path]. /// Determines whether [is season folder] [the specified path].
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="parentPath">The parentpath.</param>
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param> /// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType) private static bool IsSeasonFolder(string path, bool isTvContentType)
{ {
var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber; var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue; return seasonNumber.HasValue;
} }

View File

@@ -3,9 +3,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@@ -172,7 +171,7 @@ namespace Emby.Server.Implementations.Library
} }
}; };
IReadOnlyList<BaseItem> mediaItems; List<BaseItem> mediaItems;
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist) if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
{ {

View File

@@ -4,13 +4,13 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library; namespace Emby.Server.Implementations.Library;
@@ -43,26 +43,14 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
/// <inheritdoc /> /// <inheritdoc />
public Task Run(IProgress<double> progress, CancellationToken cancellationToken) public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{ {
var posters = GetItemsWithImageType(ImageType.Primary) var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList();
.Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path) var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList();
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => path!)
.ToList();
var backdrops = GetItemsWithImageType(ImageType.Thumb)
.Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path)
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => path!)
.ToList();
if (backdrops.Count == 0) if (backdrops.Count == 0)
{ {
// Thumb images fit better because they include the title in the image but are not provided with TMDb. // Thumb images fit better because they include the title in the image but are not provided with TMDb.
// Using backdrops as a fallback to generate an image at all // Using backdrops as a fallback to generate an image at all
_logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen"); _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen");
backdrops = GetItemsWithImageType(ImageType.Backdrop) backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList();
.Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path)
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => path!)
.ToList();
} }
_imageEncoder.CreateSplashscreen(posters, backdrops); _imageEncoder.CreateSplashscreen(posters, backdrops);
@@ -77,15 +65,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
CollapseBoxSetItems = false, CollapseBoxSetItems = false,
Recursive = true, Recursive = true,
DtoOptions = new DtoOptions(false), DtoOptions = new DtoOptions(false),
ImageTypes = [imageType], ImageTypes = new[] { imageType },
Limit = 30, Limit = 30,
// TODO max parental rating configurable // TODO max parental rating configurable
MaxParentalRating = new(10, null), MaxParentalRating = 10,
OrderBy = OrderBy = new[]
[ {
(ItemSortBy.Random, SortOrder.Ascending) (ItemSortBy.Random, SortOrder.Ascending)
], },
IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series] IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series }
}); });
} }
} }

View File

@@ -1,20 +1,17 @@
#pragma warning disable RS0030 // Do not use banned APIs
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Threading; using System.Threading;
using BitFaster.Caching.Lru; using Jellyfin.Data.Entities;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.EntityFrameworkCore;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook; using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book; using Book = MediaBrowser.Controller.Entities.Book;
@@ -25,22 +22,27 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
public class UserDataManager : IUserDataManager public class UserDataManager : IUserDataManager
{ {
private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IDbContextFactory<JellyfinDbContext> _repository; private readonly IUserManager _userManager;
private readonly FastConcurrentLru<string, UserItemData> _cache; private readonly IUserDataRepository _repository;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class. /// Initializes a new instance of the <see cref="UserDataManager"/> class.
/// </summary> /// </summary>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param>
public UserDataManager( public UserDataManager(
IServerConfigurationManager config, IServerConfigurationManager config,
IDbContextFactory<JellyfinDbContext> repository) IUserManager userManager,
IUserDataRepository repository)
{ {
_config = config; _config = config;
_userManager = userManager;
_repository = repository; _repository = repository;
_cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -57,30 +59,15 @@ namespace Emby.Server.Implementations.Library
var keys = item.GetUserDataKeys(); var keys = item.GetUserDataKeys();
using var dbContext = _repository.CreateDbContext(); var userId = user.InternalId;
using var transaction = dbContext.Database.BeginTransaction();
foreach (var key in keys) foreach (var key in keys)
{ {
userData.Key = key; _repository.SaveUserData(userId, key, userData, cancellationToken);
var userDataEntry = Map(userData, user.Id, item.Id);
if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey))
{
dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified;
}
else
{
dbContext.UserData.Add(userDataEntry);
}
} }
dbContext.SaveChanges();
transaction.Commit();
var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id); var cacheKey = GetCacheKey(userId, item.Id);
_cache.AddOrUpdate(cacheKey, userData); _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray(); // rehydrate the cached userdata
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
{ {
@@ -97,9 +84,10 @@ namespace Emby.Server.Implementations.Library
{ {
ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(item); ArgumentNullException.ThrowIfNull(item);
ArgumentNullException.ThrowIfNull(reason);
ArgumentNullException.ThrowIfNull(userDataDto); ArgumentNullException.ThrowIfNull(userDataDto);
var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null."); var userData = GetUserData(user, item);
if (userDataDto.PlaybackPositionTicks.HasValue) if (userDataDto.PlaybackPositionTicks.HasValue)
{ {
@@ -139,91 +127,33 @@ namespace Emby.Server.Implementations.Library
SaveUserData(user, item, userData, reason, CancellationToken.None); SaveUserData(user, item, userData, reason, CancellationToken.None);
} }
private UserData Map(UserItemData dto, Guid userId, Guid itemId) private UserItemData GetUserData(User user, Guid itemId, List<string> keys)
{ {
return new UserData() var userId = user.InternalId;
{
ItemId = itemId, var cacheKey = GetCacheKey(userId, itemId);
CustomDataKey = dto.Key,
Item = null, return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
User = null,
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
Likes = dto.Likes,
PlaybackPositionTicks = dto.PlaybackPositionTicks,
PlayCount = dto.PlayCount,
Played = dto.Played,
Rating = dto.Rating,
UserId = userId,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
};
} }
private static UserItemData Map(UserData dto) private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
{ {
return new UserItemData() var userData = _repository.GetUserData(internalUserId, keys);
{
Key = dto.CustomDataKey!,
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
Likes = dto.Likes,
PlaybackPositionTicks = dto.PlaybackPositionTicks,
PlayCount = dto.PlayCount,
Played = dto.Played,
Rating = dto.Rating,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
};
}
private UserItemData? GetUserData(User user, Guid itemId, List<string> keys) if (userData is not null)
{
var cacheKey = GetCacheKey(user.InternalId, itemId);
if (_cache.TryGet(cacheKey, out var data))
{ {
return data; return userData;
} }
data = GetUserDataInternal(user.Id, itemId, keys); if (keys.Count > 0)
if (data is null)
{ {
return new UserItemData() return new UserItemData
{ {
Key = keys[0], Key = keys[0]
}; };
} }
return _cache.GetOrAdd(cacheKey, _ => data); throw new UnreachableException();
}
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
{
if (keys.Count == 0)
{
return null;
}
using var context = _repository.CreateDbContext();
var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
if (userData.Length > 0)
{
var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
if (directDataReference is not null)
{
return Map(directDataReference);
}
return Map(userData.First());
}
return new UserItemData
{
Key = keys.Last()!
};
} }
/// <summary> /// <summary>
@@ -236,28 +166,20 @@ namespace Emby.Server.Implementations.Library
} }
/// <inheritdoc /> /// <inheritdoc />
public UserItemData? GetUserData(User user, BaseItem item) public UserItemData GetUserData(User user, BaseItem item)
{ {
return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData() return GetUserData(user, item.Id, item.GetUserDataKeys());
{
Key = item.GetUserDataKeys()[0],
};
} }
/// <inheritdoc /> /// <inheritdoc />
public UserItemDataDto? GetUserDataDto(BaseItem item, User user) public UserItemDataDto GetUserDataDto(BaseItem item, User user)
=> GetUserDataDto(item, null, user, new DtoOptions()); => GetUserDataDto(item, null, user, new DtoOptions());
/// <inheritdoc /> /// <inheritdoc />
public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options) public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
{ {
var userData = GetUserData(user, item); var userData = GetUserData(user, item);
if (userData is null) var dto = GetUserItemDataDto(userData);
{
return null;
}
var dto = GetUserItemDataDto(userData, item.Id);
item.FillUserDataDtoValues(dto, userData, itemDto, user, options); item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
return dto; return dto;
@@ -267,10 +189,9 @@ namespace Emby.Server.Implementations.Library
/// Converts a UserItemData to a DTOUserItemData. /// Converts a UserItemData to a DTOUserItemData.
/// </summary> /// </summary>
/// <param name="data">The data.</param> /// <param name="data">The data.</param>
/// <param name="itemId">The reference key to an Item.</param>
/// <returns>DtoUserItemData.</returns> /// <returns>DtoUserItemData.</returns>
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception> /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId) private UserItemDataDto GetUserItemDataDto(UserItemData data)
{ {
ArgumentNullException.ThrowIfNull(data); ArgumentNullException.ThrowIfNull(data);
@@ -283,7 +204,6 @@ namespace Emby.Server.Implementations.Library
Rating = data.Rating, Rating = data.Rating,
Played = data.Played, Played = data.Played,
LastPlayedDate = data.LastPlayedDate, LastPlayedDate = data.LastPlayedDate,
ItemId = itemId,
Key = data.Key Key = data.Key
}; };
} }
@@ -308,7 +228,7 @@ namespace Emby.Server.Implementations.Library
// ignore progress during the beginning // ignore progress during the beginning
positionTicks = 0; positionTicks = 0;
} }
else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= (runtimeTicks - TimeSpan.TicksPerSecond)) else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks)
{ {
// mark as completed close to the end // mark as completed close to the end
positionTicks = 0; positionTicks = 0;

View File

@@ -6,10 +6,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Jellyfin.Data; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@@ -310,40 +308,39 @@ namespace Emby.Server.Implementations.Library
} }
} }
MediaType[] mediaTypes = []; var mediaTypes = new List<MediaType>();
if (includeItemTypes.Length == 0) if (includeItemTypes.Length == 0)
{ {
HashSet<MediaType> tmpMediaTypes = [];
foreach (var parent in parents.OfType<ICollectionFolder>()) foreach (var parent in parents.OfType<ICollectionFolder>())
{ {
switch (parent.CollectionType) switch (parent.CollectionType)
{ {
case CollectionType.books: case CollectionType.books:
tmpMediaTypes.Add(MediaType.Book); mediaTypes.Add(MediaType.Book);
tmpMediaTypes.Add(MediaType.Audio); mediaTypes.Add(MediaType.Audio);
break; break;
case CollectionType.music: case CollectionType.music:
tmpMediaTypes.Add(MediaType.Audio); mediaTypes.Add(MediaType.Audio);
break; break;
case CollectionType.photos: case CollectionType.photos:
tmpMediaTypes.Add(MediaType.Photo); mediaTypes.Add(MediaType.Photo);
tmpMediaTypes.Add(MediaType.Video); mediaTypes.Add(MediaType.Video);
break; break;
case CollectionType.homevideos: case CollectionType.homevideos:
tmpMediaTypes.Add(MediaType.Photo); mediaTypes.Add(MediaType.Photo);
tmpMediaTypes.Add(MediaType.Video); mediaTypes.Add(MediaType.Video);
break; break;
default: default:
tmpMediaTypes.Add(MediaType.Video); mediaTypes.Add(MediaType.Video);
break; break;
} }
} }
mediaTypes = tmpMediaTypes.ToArray(); mediaTypes = mediaTypes.Distinct().ToList();
} }
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0 var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
? new[] ? new[]
{ {
BaseItemKind.Person, BaseItemKind.Person,
@@ -369,31 +366,12 @@ namespace Emby.Server.Implementations.Library
Limit = limit * 5, Limit = limit * 5,
IsPlayed = isPlayed, IsPlayed = isPlayed,
DtoOptions = options, DtoOptions = options,
MediaTypes = mediaTypes MediaTypes = mediaTypes.ToArray()
}; };
if (request.GroupItems) if (parents.Count == 0)
{ {
var collectionType = parents return _libraryManager.GetItemList(query, false);
.Select(parent => parent switch
{
ICollectionFolder collectionFolder => collectionFolder.CollectionType,
UserView userView => userView.CollectionType,
_ => null
})
.FirstOrDefault(type => type is not null);
if (collectionType == CollectionType.tvshows)
{
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows);
}
if (collectionType == CollectionType.music)
{
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
}
} }
return _libraryManager.GetItemList(query, parents); return _libraryManager.GetItemList(query, parents);

View File

@@ -5,44 +5,45 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class ArtistsPostScanTask.
/// </summary>
public class ArtistsPostScanTask : ILibraryPostScanTask
{ {
/// <summary> /// <summary>
/// The _library manager. /// Class ArtistsPostScanTask.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; public class ArtistsPostScanTask : ILibraryPostScanTask
private readonly ILogger<ArtistsValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public ArtistsPostScanTask(
ILibraryManager libraryManager,
ILogger<ArtistsValidator> logger,
IItemRepository itemRepo)
{ {
_libraryManager = libraryManager; /// <summary>
_logger = logger; /// The _library manager.
_itemRepo = itemRepo; /// </summary>
} private readonly ILibraryManager _libraryManager;
private readonly ILogger<ArtistsValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary> /// <summary>
/// Runs the specified progress. /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
/// </summary> /// </summary>
/// <param name="progress">The progress.</param> /// <param name="libraryManager">The library manager.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="logger">The logger.</param>
/// <returns>Task.</returns> /// <param name="itemRepo">The item repository.</param>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken) public ArtistsPostScanTask(
{ ILibraryManager libraryManager,
return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); ILogger<ArtistsValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
} }
} }

View File

@@ -10,101 +10,102 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class ArtistsValidator.
/// </summary>
public class ArtistsValidator
{ {
/// <summary> /// <summary>
/// The library manager. /// Class ArtistsValidator.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; public class ArtistsValidator
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<ArtistsValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
{ {
_libraryManager = libraryManager; /// <summary>
_logger = logger; /// The library manager.
_itemRepo = itemRepo; /// </summary>
} private readonly ILibraryManager _libraryManager;
/// <summary> /// <summary>
/// Runs the specified progress. /// The logger.
/// </summary> /// </summary>
/// <param name="progress">The progress.</param> private readonly ILogger<ArtistsValidator> _logger;
/// <param name="cancellationToken">The cancellation token.</param> private readonly IItemRepository _itemRepo;
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetAllArtistNames();
var numComplete = 0; /// <summary>
var count = names.Count; /// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
/// </summary>
foreach (var name in names) /// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
{ {
try _libraryManager = libraryManager;
{ _logger = logger;
var item = _libraryManager.GetArtist(name); _itemRepo = itemRepo;
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
} }
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery /// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{ {
IncludeItemTypes = [BaseItemKind.MusicArtist], var names = _itemRepo.GetAllArtistNames();
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>().ToList();
foreach (var item in deadEntities) var numComplete = 0;
{ var count = names.Count;
if (!item.IsAccessedByName)
foreach (var name in names)
{ {
continue; try
}
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{ {
DeleteFileLocation = false var item = _libraryManager.GetArtist(name);
},
false);
}
progress.Report(100); await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>().ToList();
foreach (var item in deadEntities)
{
if (!item.IsAccessedByName)
{
continue;
}
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
}
progress.Report(100);
}
} }
} }

View File

@@ -4,150 +4,153 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class CollectionPostScanTask.
/// </summary>
public class CollectionPostScanTask : ILibraryPostScanTask
{ {
private readonly ILibraryManager _libraryManager;
private readonly ICollectionManager _collectionManager;
private readonly ILogger<CollectionPostScanTask> _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class. /// Class CollectionPostScanTask.
/// </summary> /// </summary>
/// <param name="libraryManager">The library manager.</param> public class CollectionPostScanTask : ILibraryPostScanTask
/// <param name="collectionManager">The collection manager.</param>
/// <param name="logger">The logger.</param>
public CollectionPostScanTask(
ILibraryManager libraryManager,
ICollectionManager collectionManager,
ILogger<CollectionPostScanTask> logger)
{ {
_libraryManager = libraryManager; private readonly ILibraryManager _libraryManager;
_collectionManager = collectionManager; private readonly ICollectionManager _collectionManager;
_logger = logger; private readonly ILogger<CollectionPostScanTask> _logger;
}
/// <summary> /// <summary>
/// Runs the specified progress. /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
/// </summary> /// </summary>
/// <param name="progress">The progress.</param> /// <param name="libraryManager">The library manager.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="collectionManager">The collection manager.</param>
/// <returns>Task.</returns> /// <param name="logger">The logger.</param>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) public CollectionPostScanTask(
{ ILibraryManager libraryManager,
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>(); ICollectionManager collectionManager,
ILogger<CollectionPostScanTask> logger)
foreach (var library in _libraryManager.RootFolder.Children)
{ {
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection) _libraryManager = libraryManager;
{ _collectionManager = collectionManager;
continue; _logger = logger;
}
var startIndex = 0;
var pagesize = 1000;
while (true)
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = [MediaType.Video],
IncludeItemTypes = [BaseItemKind.Movie],
IsVirtualItem = false,
OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)],
Parent = library,
StartIndex = startIndex,
Limit = pagesize,
Recursive = true
});
foreach (var m in movies)
{
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
{
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{
movieList.Add(movie.Id);
}
else
{
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
}
}
}
if (movies.Count < pagesize)
{
break;
}
startIndex += pagesize;
}
} }
var numComplete = 0; /// <summary>
var count = collectionNameMoviesMap.Count; /// Runs the specified progress.
/// </summary>
if (count == 0) /// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{ {
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
foreach (var library in _libraryManager.RootFolder.Children)
{
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
{
continue;
}
var startIndex = 0;
var pagesize = 1000;
while (true)
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = new[] { MediaType.Video },
IncludeItemTypes = new[] { BaseItemKind.Movie },
IsVirtualItem = false,
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
Parent = library,
StartIndex = startIndex,
Limit = pagesize,
Recursive = true
});
foreach (var m in movies)
{
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
{
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{
movieList.Add(movie.Id);
}
else
{
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
}
}
}
if (movies.Count < pagesize)
{
break;
}
startIndex += pagesize;
}
}
var numComplete = 0;
var count = collectionNameMoviesMap.Count;
if (count == 0)
{
progress.Report(100);
return;
}
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.BoxSet },
CollapseBoxSetItems = false,
Recursive = true
});
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
{
try
{
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
if (boxSet is null)
{
// won't automatically create collection if only one movie in it
if (movieIds.Count >= 2)
{
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
Name = collectionName,
IsLocked = true
});
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
}
}
else
{
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
}
}
progress.Report(100); progress.Report(100);
return;
} }
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.BoxSet],
CollapseBoxSetItems = false,
Recursive = true
});
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
{
try
{
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
if (boxSet is null)
{
// won't automatically create collection if only one movie in it
if (movieIds.Count >= 2)
{
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
Name = collectionName,
}).ConfigureAwait(false);
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
}
}
else
{
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
}
}
progress.Report(100);
} }
} }

View File

@@ -5,44 +5,45 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class GenresPostScanTask.
/// </summary>
public class GenresPostScanTask : ILibraryPostScanTask
{ {
/// <summary> /// <summary>
/// The _library manager. /// Class GenresPostScanTask.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; public class GenresPostScanTask : ILibraryPostScanTask
private readonly ILogger<GenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public GenresPostScanTask(
ILibraryManager libraryManager,
ILogger<GenresValidator> logger,
IItemRepository itemRepo)
{ {
_libraryManager = libraryManager; /// <summary>
_logger = logger; /// The _library manager.
_itemRepo = itemRepo; /// </summary>
} private readonly ILibraryManager _libraryManager;
private readonly ILogger<GenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary> /// <summary>
/// Runs the specified progress. /// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
/// </summary> /// </summary>
/// <param name="progress">The progress.</param> /// <param name="libraryManager">The library manager.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="logger">The logger.</param>
/// <returns>Task.</returns> /// <param name="itemRepo">The item repository.</param>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken) public GenresPostScanTask(
{ ILibraryManager libraryManager,
return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); ILogger<GenresValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
} }
} }

View File

@@ -1,103 +1,81 @@
using System; using System;
using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class GenresValidator.
/// </summary>
public class GenresValidator
{ {
/// <summary> /// <summary>
/// The library manager. /// Class GenresValidator.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; public class GenresValidator
private readonly IItemRepository _itemRepo;
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<GenresValidator> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="GenresValidator"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
{ {
_libraryManager = libraryManager; /// <summary>
_logger = logger; /// The library manager.
_itemRepo = itemRepo; /// </summary>
} private readonly ILibraryManager _libraryManager;
private readonly IItemRepository _itemRepo;
/// <summary> /// <summary>
/// Runs the specified progress. /// The logger.
/// </summary> /// </summary>
/// <param name="progress">The progress.</param> private readonly ILogger<GenresValidator> _logger;
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetGenreNames();
var numComplete = 0; /// <summary>
var count = names.Count; /// Initializes a new instance of the <see cref="GenresValidator"/> class.
/// </summary>
foreach (var name in names) /// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
{ {
try _libraryManager = libraryManager;
{ _logger = logger;
var item = _libraryManager.GetGenre(name); _itemRepo = itemRepo;
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {GenreName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
} }
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery /// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{ {
IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre], var names = _itemRepo.GetGenreNames();
IsDeadGenre = true,
IsLocked = false
});
foreach (var item in deadEntities) var numComplete = 0;
{ var count = names.Count;
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
_libraryManager.DeleteItem( foreach (var name in names)
item, {
new DeleteOptions try
{ {
DeleteFileLocation = false var item = _libraryManager.GetGenre(name);
},
false);
}
progress.Report(100); await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {GenreName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
progress.Report(100);
}
} }
} }

View File

@@ -5,44 +5,45 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class MusicGenresPostScanTask.
/// </summary>
public class MusicGenresPostScanTask : ILibraryPostScanTask
{ {
/// <summary> /// <summary>
/// The library manager. /// Class MusicGenresPostScanTask.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; public class MusicGenresPostScanTask : ILibraryPostScanTask
private readonly ILogger<MusicGenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public MusicGenresPostScanTask(
ILibraryManager libraryManager,
ILogger<MusicGenresValidator> logger,
IItemRepository itemRepo)
{ {
_libraryManager = libraryManager; /// <summary>
_logger = logger; /// The library manager.
_itemRepo = itemRepo; /// </summary>
} private readonly ILibraryManager _libraryManager;
private readonly ILogger<MusicGenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary> /// <summary>
/// Runs the specified progress. /// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
/// </summary> /// </summary>
/// <param name="progress">The progress.</param> /// <param name="libraryManager">The library manager.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="logger">The logger.</param>
/// <returns>Task.</returns> /// <param name="itemRepo">The item repository.</param>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken) public MusicGenresPostScanTask(
{ ILibraryManager libraryManager,
return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); ILogger<MusicGenresValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
} }
} }

View File

@@ -5,76 +5,77 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class MusicGenresValidator.
/// </summary>
public class MusicGenresValidator
{ {
/// <summary> /// <summary>
/// The library manager. /// Class MusicGenresValidator.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; public class MusicGenresValidator
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<MusicGenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
{ {
_libraryManager = libraryManager; /// <summary>
_logger = logger; /// The library manager.
_itemRepo = itemRepo; /// </summary>
} private readonly ILibraryManager _libraryManager;
/// <summary> /// <summary>
/// Runs the specified progress. /// The logger.
/// </summary> /// </summary>
/// <param name="progress">The progress.</param> private readonly ILogger<MusicGenresValidator> _logger;
/// <param name="cancellationToken">The cancellation token.</param> private readonly IItemRepository _itemRepo;
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetMusicGenreNames();
var numComplete = 0; /// <summary>
var count = names.Count; /// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
/// </summary>
foreach (var name in names) /// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
{ {
try _libraryManager = libraryManager;
{ _logger = logger;
var item = _libraryManager.GetMusicGenre(name); _itemRepo = itemRepo;
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {GenreName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
} }
progress.Report(100); /// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetMusicGenreNames();
var numComplete = 0;
var count = names.Count;
foreach (var name in names)
{
try
{
var item = _libraryManager.GetMusicGenre(name);
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {GenreName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
progress.Report(100);
}
} }
} }

View File

@@ -1,5 +1,5 @@
using System; using System;
using System.Linq; using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@@ -9,112 +9,119 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class PeopleValidator.
/// </summary>
public class PeopleValidator
{ {
/// <summary> /// <summary>
/// The _library manager. /// Class PeopleValidator.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; public class PeopleValidator
/// <summary>
/// The _logger.
/// </summary>
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="PeopleValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</param>
public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
{ {
_libraryManager = libraryManager; /// <summary>
_logger = logger; /// The _library manager.
_fileSystem = fileSystem; /// </summary>
} private readonly ILibraryManager _libraryManager;
/// <summary> /// <summary>
/// Validates the people. /// The _logger.
/// </summary> /// </summary>
/// <param name="cancellationToken">The cancellation token.</param> private readonly ILogger _logger;
/// <param name="progress">The progress.</param>
/// <returns>Task.</returns>
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
{
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
var numComplete = 0; private readonly IFileSystem _fileSystem;
var numPeople = people.Count; /// <summary>
/// Initializes a new instance of the <see cref="PeopleValidator" /> class.
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2)); /// </summary>
/// <param name="libraryManager">The library manager.</param>
_logger.LogDebug("Will refresh {Amount} people", numPeople); /// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</param>
foreach (var person in people) public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
{ {
cancellationToken.ThrowIfCancellationRequested(); _libraryManager = libraryManager;
_logger = logger;
_fileSystem = fileSystem;
}
try /// <summary>
/// Validates the people.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param>
/// <returns>Task.</returns>
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
{
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
var numComplete = 0;
var numPeople = people.Count;
_logger.LogDebug("Will refresh {0} people", numPeople);
foreach (var person in people)
{ {
var item = _libraryManager.GetPerson(person); cancellationToken.ThrowIfCancellationRequested();
if (item is null)
try
{ {
_logger.LogWarning("Failed to get person: {Name}", person); var item = _libraryManager.GetPerson(person);
continue; if (item is null)
{
_logger.LogWarning("Failed to get person: {Name}", person);
continue;
}
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
};
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
} }
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) // Update progress
{ numComplete++;
ImageRefreshMode = MetadataRefreshMode.ValidationOnly, double percent = numComplete;
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly percent /= numPeople;
};
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); progress.Report(100 * percent);
} }
catch (OperationCanceledException)
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{ {
throw; IncludeItemTypes = [BaseItemKind.Person],
} IsDeadPerson = true,
catch (Exception ex) IsLocked = false
});
foreach (var item in deadEntities)
{ {
_logger.LogError(ex, "Error validating IBN entry {Person}", person); _logger.LogInformation(
"Deleting dead {2} {0} {1}.",
item.Id.ToString("N", CultureInfo.InvariantCulture),
item.Name,
item.GetType().Name);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
} }
// Update progress progress.Report(100);
numComplete++;
double percent = numComplete;
percent /= numPeople;
subProgress.Report(100 * percent); _logger.LogInformation("People validation complete");
} }
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Person],
IsDeadPerson = true,
IsLocked = false
});
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
var i = 0;
foreach (var item in deadEntities.Chunk(500))
{
_libraryManager.DeleteItemsUnsafeFast(item);
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
}
progress.Report(100);
_logger.LogInformation("People validation complete");
} }
} }

View File

@@ -5,45 +5,46 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class MusicGenresPostScanTask.
/// </summary>
public class StudiosPostScanTask : ILibraryPostScanTask
{ {
/// <summary> /// <summary>
/// The _library manager. /// Class MusicGenresPostScanTask.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; public class StudiosPostScanTask : ILibraryPostScanTask
private readonly ILogger<StudiosValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public StudiosPostScanTask(
ILibraryManager libraryManager,
ILogger<StudiosValidator> logger,
IItemRepository itemRepo)
{ {
_libraryManager = libraryManager; /// <summary>
_logger = logger; /// The _library manager.
_itemRepo = itemRepo; /// </summary>
} private readonly ILibraryManager _libraryManager;
/// <summary> private readonly ILogger<StudiosValidator> _logger;
/// Runs the specified progress. private readonly IItemRepository _itemRepo;
/// </summary>
/// <param name="progress">The progress.</param> /// <summary>
/// <param name="cancellationToken">The cancellation token.</param> /// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
/// <returns>Task.</returns> /// </summary>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken) /// <param name="libraryManager">The library manager.</param>
{ /// <param name="logger">The logger.</param>
return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); /// <param name="itemRepo">The item repository.</param>
public StudiosPostScanTask(
ILibraryManager libraryManager,
ILogger<StudiosValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
} }
} }

View File

@@ -8,97 +8,98 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators; namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// Class StudiosValidator.
/// </summary>
public class StudiosValidator
{ {
/// <summary> /// <summary>
/// The library manager. /// Class StudiosValidator.
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; public class StudiosValidator
private readonly IItemRepository _itemRepo;
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<StudiosValidator> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="StudiosValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
{ {
_libraryManager = libraryManager; /// <summary>
_logger = logger; /// The library manager.
_itemRepo = itemRepo; /// </summary>
} private readonly ILibraryManager _libraryManager;
/// <summary> private readonly IItemRepository _itemRepo;
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetStudioNames();
var numComplete = 0; /// <summary>
var count = names.Count; /// The logger.
/// </summary>
private readonly ILogger<StudiosValidator> _logger;
foreach (var name in names) /// <summary>
/// Initializes a new instance of the <see cref="StudiosValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
{ {
try _libraryManager = libraryManager;
{ _logger = logger;
var item = _libraryManager.GetStudio(name); _itemRepo = itemRepo;
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {StudioName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
} }
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery /// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{ {
IncludeItemTypes = [BaseItemKind.Studio], var names = _itemRepo.GetStudioNames();
IsDeadStudio = true,
IsLocked = false
});
foreach (var item in deadEntities) var numComplete = 0;
{ var count = names.Count;
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
_libraryManager.DeleteItem( foreach (var name in names)
item, {
new DeleteOptions try
{ {
DeleteFileLocation = false var item = _libraryManager.GetStudio(name);
},
false);
}
progress.Report(100); await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {StudioName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Studio },
IsDeadStudio = true,
IsLocked = false
});
foreach (var item in deadEntities)
{
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
}
progress.Report(100);
}
} }
} }

View File

@@ -129,11 +129,5 @@
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.", "TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
"TaskAudioNormalization": "Odio Normalisering", "TaskAudioNormalization": "Odio Normalisering",
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon", "TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
"TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.", "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie."
"TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
"TaskExtractMediaSegments": "Media Segment Skandeer",
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
} }

View File

@@ -16,7 +16,7 @@
"Folders": "المجلدات", "Folders": "المجلدات",
"Genres": "التصنيفات", "Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم", "HeaderAlbumArtists": "فناني الألبوم",
"HeaderContinueWatching": "إستئناف المشاهدة", "HeaderContinueWatching": "استئناف المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة", "HeaderFavoriteEpisodes": "الحلقات المفضلة",
@@ -31,7 +31,7 @@
"ItemRemovedWithName": "أُزيل {0} من المكتبة", "ItemRemovedWithName": "أُزيل {0} من المكتبة",
"LabelIpAddressValue": "عنوان الآي بي: {0}", "LabelIpAddressValue": "عنوان الآي بي: {0}",
"LabelRunningTimeValue": "مدة التشغيل: {0}", "LabelRunningTimeValue": "مدة التشغيل: {0}",
"Latest": "الأحدث", "Latest": "أحدث",
"MessageApplicationUpdated": "حُدث خادم Jellyfin", "MessageApplicationUpdated": "حُدث خادم Jellyfin",
"MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}", "MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}", "MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
@@ -52,7 +52,7 @@
"NotificationOptionInstallationFailed": "فشل في التثبيت", "NotificationOptionInstallationFailed": "فشل في التثبيت",
"NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا", "NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
"NotificationOptionPluginError": "فشل في الملحق", "NotificationOptionPluginError": "فشل في الملحق",
"NotificationOptionPluginInstalled": "ثُبتت الملحق", "NotificationOptionPluginInstalled": "ثُبتت المكونات الإضافية",
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق", "NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق", "NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
"NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم", "NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
@@ -90,10 +90,10 @@
"UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}", "UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}", "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط", "ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
"ValueSpecialEpisodeName": "حلقة خاصه - {0}", "ValueSpecialEpisodeName": "حلقه خاصه - {0}",
"VersionNumber": "الإصدار {0}", "VersionNumber": "الإصدار {0}",
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.", "TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
"TaskCleanCache": "حذف الملفات المؤقتة", "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة",
"TasksChannelsCategory": "قنوات الإنترنت", "TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "مكتبة", "TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة", "TasksMaintenanceCategory": "صيانة",
@@ -125,18 +125,11 @@
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
"External": "خارجي", "External": "خارجي",
"HearingImpaired": "ضعاف السمع", "HearingImpaired": "ضعاف السمع",
"TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة", "TaskRefreshTrickplayImages": "توليد صور Trickplay",
"TaskRefreshTrickplayImagesDescription": "يُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.", "TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل", "TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.", "TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
"TaskAudioNormalization": سوية الصوت", "TaskAudioNormalization": طبيع الصوت",
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.", "TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
"TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة", "TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة"
"TaskDownloadMissingLyricsDescription": "كلمات",
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
"CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
} }

View File

@@ -1,22 +1,22 @@
{ {
"Sync": "Сінхранізаваць", "Sync": "Сінхранізаваць",
"Playlists": "Плэй-лісты", "Playlists": "Плэйлісты",
"Latest": "Апошняе", "Latest": "Апошні",
"LabelIpAddressValue": "IP-адрас: {0}", "LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} даданы ў бібліятэку", "ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены", "MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана", "NotificationOptionApplicationUpdateInstalled": "Абнаўленне прыкладання ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны", "PluginInstalledWithName": "{0} быў усталяваны",
"UserCreatedWithName": "Карыстальнік {0} быў створаны", "UserCreatedWithName": "Карыстальнік {0} быў створаны",
"Albums": "Альбомы", "Albums": "Альбомы",
"Application": "Праграма", "Application": "Прыкладанне",
"AuthenticationSucceededWithUserName": "{0} паспяхова аўтарызаваны", "AuthenticationSucceededWithUserName": "{0} паспяхова аўтэнтыфікаваны",
"Channels": "Каналы", "Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}", "ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі", "Collections": "Калекцыі",
"Default": "Па змаўчанні", "Default": "Па змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Тэчкі", "Folders": "Папкі",
"Favorites": "Абранае", "Favorites": "Абранае",
"External": "Знешні", "External": "Знешні",
"Genres": "Жанры", "Genres": "Жанры",
@@ -29,18 +29,18 @@
"HeaderAlbumArtists": "Выканаўцы альбома", "HeaderAlbumArtists": "Выканаўцы альбома",
"LabelRunningTimeValue": "Працягласць: {0}", "LabelRunningTimeValue": "Працягласць: {0}",
"HomeVideos": "Хатнія відэа", "HomeVideos": "Хатнія відэа",
"ItemRemovedWithName": "{0} выдалены з бібліятэкі", "ItemRemovedWithName": "{0} быў выдалены з бібліятэкі",
"MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да версіі {0}", "MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да {0}",
"Movies": "Фільмы", "Movies": "Фільмы",
"Music": "Музыка", "Music": "Музыка",
"MusicVideos": "Музычныя кліпы", "MusicVideos": "Музычныя кліпы",
"NameInstallFailed": "Усталяванне {0} не атрымалася", "NameInstallFailed": "Устаноўка {0} не атрымалася",
"NameSeasonNumber": "Сезон {0}", "NameSeasonNumber": "Сезон {0}",
"NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне праграмы", "NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне прыкладання",
"NotificationOptionPluginInstalled": "Плагін усталяваны", "NotificationOptionPluginInstalled": "Плагін усталяваны",
"NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна ўсталявана", "NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна усталявана",
"NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера", "NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера",
"Photos": отаздымкі", "Photos": атаграфіі",
"Plugin": "Плагін", "Plugin": "Плагін",
"PluginUninstalledWithName": "{0} быў выдалены", "PluginUninstalledWithName": "{0} быў выдалены",
"PluginUpdatedWithName": "{0} быў абноўлены", "PluginUpdatedWithName": "{0} быў абноўлены",
@@ -54,16 +54,16 @@
"Artists": "Выканаўцы", "Artists": "Выканаўцы",
"UserOfflineFromDevice": "{0} адлучыўся ад {1}", "UserOfflineFromDevice": "{0} адлучыўся ад {1}",
"UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}", "UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
"TaskCleanActivityLogDescription": "Выдаляе запісы старэйшыя за зададзены ўзрост ў журнале актыўнасці.", "TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.",
"TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.", "TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.",
"TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.", "TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
"TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія сканфігураваныя на аўтаматычнае абнаўленне.", "TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія настроены на аўтаматычнае абнаўленне.",
"TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.", "TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.",
"TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метададзеных.", "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субтытры на аснове канфігурацыі метададзеных.",
"TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых зменаў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць выдайнасць.", "TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых змяненняў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць прадукцыйнасць.",
"TaskKeyframeExtractor": "Экстрактар ключавых кадраў", "TaskKeyframeExtractor": "Экстрактар ключавых кадраў",
"TasksApplicationCategory": "Праграма", "TasksApplicationCategory": "Прыкладанне",
"AppDeviceValues": "Праграма: {0}, Прылада: {1}", "AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}",
"Books": "Кнігі", "Books": "Кнігі",
"CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}", "CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}",
"DeviceOfflineWithName": "{0} адлучыўся", "DeviceOfflineWithName": "{0} адлучыўся",
@@ -74,7 +74,7 @@
"HeaderFavoriteArtists": "Абраныя выканаўцы", "HeaderFavoriteArtists": "Абраныя выканаўцы",
"HearingImpaired": "Са слабым слыхам", "HearingImpaired": "Са слабым слыхам",
"Inherit": "Атрымаць у спадчыну", "Inherit": "Атрымаць у спадчыну",
"MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера (секцыя {0}) абноўлена", "MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера {0} абноўлена",
"MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена", "MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена",
"MixedContent": "Змешаны змест", "MixedContent": "Змешаны змест",
"NameSeasonUnknown": "Невядомы сезон", "NameSeasonUnknown": "Невядомы сезон",
@@ -92,50 +92,48 @@
"NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена", "NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена",
"ScheduledTaskFailedWithName": "{0} не атрымалася", "ScheduledTaskFailedWithName": "{0} не атрымалася",
"ScheduledTaskStartedWithName": "{0} пачалося", "ScheduledTaskStartedWithName": "{0} пачалося",
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску", "ServerNameNeedsToBeRestarted": "{0} трэба перазапусціць",
"Shows": "Шоу", "Shows": "Шоу",
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.", "StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
"SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}", "SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
"TvShows": "Тэлепраграма", "TvShows": "ТБ-шоу",
"Undefined": "Нявызначана", "Undefined": "Нявызначана",
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны", "UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
"UserOnlineFromDevice": "{0} падключаны з {1}", "UserOnlineFromDevice": "{0} падключаны з {1}",
"UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}", "UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}",
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}", "UserStartedPlayingItemWithValues": "{0} грае {1} на {2}",
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}", "UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку", "ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
"ValueSpecialEpisodeName": "Спецэпізод - {0}", "ValueSpecialEpisodeName": "Спецэпізод - {0}",
"VersionNumber": "Версія {0}", "VersionNumber": "Версія {0}",
"TasksMaintenanceCategory": "Абслугоўванне", "TasksMaintenanceCategory": "Абслугоўванне",
"TasksLibraryCategory": "Бібліятэка", "TasksLibraryCategory": "Медыятэка",
"TasksChannelsCategory": "Інтэрнэт-каналы", "TasksChannelsCategory": "Інтэрнэт-каналы",
"TaskCleanActivityLog": "Ачысціць журнал актыўнасці", "TaskCleanActivityLog": "Ачысціць журнал актыўнасці",
"TaskCleanCache": "Ачысціць кэш", "TaskCleanCache": "Ачысціць кэш",
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.", "TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
"TaskRefreshChapterImages": "Вынуць выявы раздзелаў", "TaskRefreshChapterImages": "Выняць выявы раздзелаў",
"TaskRefreshLibrary": "Сканаваць бібліятэку", "TaskRefreshLibrary": "Сканіраваць медыятэку",
"TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.", "TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
"TaskCleanLogs": "Ачысціць журнал", "TaskCleanLogs": "Ачысціць часопіс",
"TaskRefreshPeople": "Абнавіць выканаўцаў", "TaskRefreshPeople": "Абнавіць людзей",
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.", "TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
"TaskUpdatePlugins": "Абнавіць плагіны", "TaskUpdatePlugins": "Абнавіць плагіны",
"TaskCleanTranscode": "Ачысціць каталог перакадзіравання", "TaskCleanTranscode": "Ачысціць каталог перакадзіравання",
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.", "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
"TaskRefreshChannels": "Абнавіць каналы", "TaskRefreshChannels": "Абнавіць каналы",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры", "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.", "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.",
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay", "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.", "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты", "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.", "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.",
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.", "TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.",
"TaskAudioNormalization": "Нармалізацыя гуку", "TaskAudioNormalization": "Нармалізацыя гуку",
"TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.", "TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Перамяшчае існуючыя файлы trickplay у адпаведнасці з наладамі бібліятэкі.", "TaskMoveTrickplayImagesDescription": "Перамяшчае існуючыя файлы trickplay у адпаведнасці з наладамі бібліятэкі.",
"TaskDownloadMissingLyrics": "Спампаваць адсутныя тэксты песняў", "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень",
"TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песняў", "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень",
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay"
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
"CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
} }

View File

@@ -121,7 +121,7 @@
"TaskCleanActivityLog": "Изчисти дневника с активност", "TaskCleanActivityLog": "Изчисти дневника с активност",
"TaskOptimizeDatabaseDescription": "Прави базата данни по-компактна и освобождава място. Пускането на тази задача след сканиране на библиотеката или правене на други промени, свързани с модификации на базата данни, може да подобри производителността.", "TaskOptimizeDatabaseDescription": "Прави базата данни по-компактна и освобождава място. Пускането на тази задача след сканиране на библиотеката или правене на други промени, свързани с модификации на базата данни, може да подобри производителността.",
"TaskOptimizeDatabase": "Оптимизирай базата данни", "TaskOptimizeDatabase": "Оптимизирай базата данни",
"TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен HLS списък . Задачата може да отнеме много време.", "TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.",
"TaskKeyframeExtractor": "Извличане на ключови кадри", "TaskKeyframeExtractor": "Извличане на ключови кадри",
"External": "Външен", "External": "Външен",
"HearingImpaired": "Увреден слух", "HearingImpaired": "Увреден слух",
@@ -129,14 +129,8 @@
"TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.", "TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.",
"TaskDownloadMissingLyrics": "Свали липсващи текстове", "TaskDownloadMissingLyrics": "Свали липсващи текстове",
"TaskDownloadMissingLyricsDescription": "Свали текстове за песни", "TaskDownloadMissingLyricsDescription": "Свали текстове за песни",
"TaskCleanCollectionsAndPlaylists": "Изчисти колекциите и плейлистите", "TaskCleanCollectionsAndPlaylists": "Изчисти колекциите и плейлистовете",
"TaskCleanCollectionsAndPlaylistsDescription": "Премахни несъществуващи файлове в колекциите и плейлистите.", "TaskCleanCollectionsAndPlaylistsDescription": "Премахни несъществуващи файлове в колекциите и плейлистите.",
"TaskAudioNormalization": "Нормализиране на звука", "TaskAudioNormalization": "Нормализиране на звука",
"TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука.", "TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука."
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
"TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения",
"TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
"TaskExtractMediaSegments": "Сканиране за сегменти",
"CleanupUserDataTask": "Задача за почистване на потребителски данни",
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни."
} }

View File

@@ -6,29 +6,29 @@
"Channels": "চ্যানেলসমূহ", "Channels": "চ্যানেলসমূহ",
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে", "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "পুস্তকসমূহ", "Books": "পুস্তকসমূহ",
"AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন", "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
"Artists": "শিল্পীগণ", "Artists": "শিল্পীগণ",
"Application": "অ্যাপ্লিকেশন", "Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামসমূহ", "Albums": "অ্যালবামসমূহ",
"HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো", "HeaderFavoriteEpisodes": "প্রি পর্বগুলো",
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন", "HeaderContinueWatching": "দেখতে থাকুন",
"HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ", "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
"Genres": "ধরণ", "Genres": "শৈলীধারাসমূহ",
"Folders": "ফোল্ডারসমূহ", "Folders": "ফোল্ডারসমূহ",
"Favorites": "পছন্দসমূহ", "Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে", "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}", "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
"VersionNumber": "সংস্করণ {0}", "VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}", "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে", "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
"UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}", "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
"UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}", "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
"UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে", "UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে",
"UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে", "UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে",
"UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে", "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন",
"UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে", "UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে",
"UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না", "UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না",
"UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে", "UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে",
"UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে", "UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে",
@@ -36,28 +36,28 @@
"User": "ব্যবহারকারী", "User": "ব্যবহারকারী",
"TvShows": "টিভি শোগুলো", "TvShows": "টিভি শোগুলো",
"System": "সিস্টেম", "System": "সিস্টেম",
"Sync": "সমন্বয় করুন", "Sync": "সমলয় স্থাপন",
"SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে", "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।", "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
"Songs": "সঙ্গীত সমূহ", "Songs": "সঙ্গীতসমূহ",
"Shows": "শো সমূহ", "Shows": "টিভি পর্ব",
"ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন", "ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
"ScheduledTaskStartedWithName": "{0} শুরু হয়েছে", "ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
"ScheduledTaskFailedWithName": "{0} ব্যর্থ", "ScheduledTaskFailedWithName": "{0} ব্যর্থ",
"ProviderValue": "প্রদানকারী: {0}", "ProviderValue": "প্রদানকারী: {0}",
"PluginUpdatedWithName": "{0} আপডেট করা হয়েছে", "PluginUpdatedWithName": "{0} আপডেট করা হয়েছে",
"PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে", "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
"PluginInstalledWithName": "{0} ইন্সটল হয়েছে", "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
"Plugin": "প্লাগিন", "Plugin": "প্লাগিন",
"Playlists": "প্লে লিস্ট সমূহ", "Playlists": "প্লে লিস্ট সমূহ",
"Photos": "ছবিসমূহ", "Photos": "চিত্রসমূহ",
"NotificationOptionVideoPlaybackStopped": "ভিডিও প্লেব্যাক বন্ধ হয়েছে", "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
"NotificationOptionVideoPlayback": "ভিডিও প্লেব্যাক শুরু হয়েছে", "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হেছে",
"NotificationOptionUserLockedOut": "ব্যবহারকারী লক আউট হয়েছে", "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
"NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ", "NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ",
"NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে", "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক",
"NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে", "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে",
"NotificationOptionPluginUninstalled": "প্লাগিন আনইনষ্টল হয়েছে", "NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে",
"NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে", "NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে",
"NotificationOptionPluginError": "প্লাগিন ব্যর্থ", "NotificationOptionPluginError": "প্লাগিন ব্যর্থ",
"NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে", "NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে",
@@ -76,8 +76,8 @@
"Movies": "চলচ্চিত্রসমূহ", "Movies": "চলচ্চিত্রসমূহ",
"MixedContent": "মিশ্র কন্টেন্ট", "MixedContent": "মিশ্র কন্টেন্ট",
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে", "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
"HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো", "HeaderRecordingGroups": "রেকর্ডিং দল",
"MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে", "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
"MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে", "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
"MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে", "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
"Latest": "সর্বশেষ", "Latest": "সর্বশেষ",
@@ -85,57 +85,45 @@
"LabelIpAddressValue": "আইপি এড্রেস: {0}", "LabelIpAddressValue": "আইপি এড্রেস: {0}",
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে", "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে", "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
"Inherit": "উত্তরাধিকারসূত্র থেকে গ্রহণ করুন", "Inherit": "থেকে পাওয়া",
"HomeVideos": "হোম ভিডিও", "HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে", "HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি", "HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো", "HeaderFavoriteSongs": "প্রিয় গানগুলো",
"HeaderFavoriteShows": "প্রিয় শোগুলো", "HeaderFavoriteShows": "প্রিয় শোগুলো",
"TasksLibraryCategory": "লাইব্রেরি", "TasksLibraryCategory": "গ্রন্থাগার",
"TasksMaintenanceCategory": "রক্ষণাবেক্ষণ", "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
"TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি", "TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি",
"TaskRefreshChapterImagesDescription": "যেসব ভিডিওতে চ্যাপ্টার রয়েছে, তাদের জন্য থাম্বনেইল তৈরি করবে।", "TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।",
"TaskRefreshChapterImages": "্যাপ্টার ইমেজ বের করুন", "TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন",
"TaskCleanCacheDescription": "সিস্টেমের প্রয়োজনীয় ক্যাশ ফাইলগুল মুছে ফেলবে।", "TaskCleanCacheDescription": "সিস্টেমের প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।",
"TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি", "TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি",
"TasksChannelsCategory": "ইন্টারনেট চ্যানেল", "TasksChannelsCategory": "ইন্টারনেট চ্যানেল",
"TasksApplicationCategory": "অ্যাপ্লিকেশন", "TasksApplicationCategory": "আবেদন",
"TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।", "TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।",
"TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন", "TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন",
"TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।", "TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।",
"TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন", "TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন",
"TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলবে।", "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।",
"TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন", "TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন",
"TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।", "TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।",
"TaskUpdatePlugins": "আপডেট প্লাগইন", "TaskUpdatePlugins": "প্লাগইন আপডেট করুন",
"TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করবে।", "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।",
"TaskRefreshPeople": "ব্যক্তিদের তথ্য রিফ্রেশ", "TaskRefreshPeople": "পিপল রিফ্রেশ করুন",
"TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলবে।", "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।",
"TaskCleanLogs": "ক্লিন লগ ডিরেক্টরি", "TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন",
"TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করবে।", "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
"Undefined": "অসঙ্গায়িত", "Undefined": "অসঙ্গায়িত",
"Forced": "জোরকরে", "Forced": "জোরকরে",
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের অ্যাক্টিভিটি লগ মুছে দিবে।", "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
"TaskCleanActivityLog": "অ্যাক্টিভিটি লগ মুছুন", "TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
"Default": "ডিফল্ট", "Default": "ডিফল্ট",
"HearingImpaired": "শ্রবণ প্রতিবন্ধী", "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
"TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।", "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
"External": "বাহ্যিক", "External": "বাহ্যিক",
"TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস", "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক", "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।", "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি", "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন",
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।", "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।"
"TaskDownloadMissingLyricsDescription": "গানের জন্য লিরিকস ডাউনলোড করুন",
"TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
"TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্ট বের করে বা অর্জন করে।",
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
"TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।",
"TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।",
"CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।",
"TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন",
"TaskAudioNormalization": "অডিও নর্মলাইজেশন",
"CleanupUserDataTask": "ইউজার ডেটা ক্লিনআপ কাজ"
} }

View File

@@ -13,10 +13,10 @@
"DeviceOnlineWithName": "{0} està connectat", "DeviceOnlineWithName": "{0} està connectat",
"FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}", "FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
"Favorites": "Preferits", "Favorites": "Preferits",
"Folders": "Directoris", "Folders": "Carpetes",
"Genres": "Gèneres", "Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes de l'àlbum", "HeaderAlbumArtists": "Artistes de l'àlbum",
"HeaderContinueWatching": "Continueu mirant", "HeaderContinueWatching": "Continuar veient",
"HeaderFavoriteAlbums": "Àlbums preferits", "HeaderFavoriteAlbums": "Àlbums preferits",
"HeaderFavoriteArtists": "Artistes preferits", "HeaderFavoriteArtists": "Artistes preferits",
"HeaderFavoriteEpisodes": "Episodis preferits", "HeaderFavoriteEpisodes": "Episodis preferits",
@@ -24,13 +24,13 @@
"HeaderFavoriteSongs": "Cançons preferides", "HeaderFavoriteSongs": "Cançons preferides",
"HeaderLiveTV": "TV en directe", "HeaderLiveTV": "TV en directe",
"HeaderNextUp": "A continuació", "HeaderNextUp": "A continuació",
"HeaderRecordingGroups": "Grups musicals", "HeaderRecordingGroups": "Grups d'enregistrament",
"HomeVideos": "Vídeos domèstics", "HomeVideos": "Vídeos domèstics",
"Inherit": "Heretat", "Inherit": "Hereta",
"ItemAddedWithName": "{0} s'ha afegit a la mediateca", "ItemAddedWithName": "{0} ha sigut afegit a la biblioteca",
"ItemRemovedWithName": "{0} s'ha eliminat de la mediateca", "ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca",
"LabelIpAddressValue": "Adreça IP: {0}", "LabelIpAddressValue": "Adreça IP: {0}",
"LabelRunningTimeValue": "Temps en marxa: {0}", "LabelRunningTimeValue": "Temps en funcionament: {0}",
"Latest": "Darrers", "Latest": "Darrers",
"MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat", "MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
"MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}", "MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
@@ -43,9 +43,9 @@
"NameInstallFailed": "{0} instal·lació fallida", "NameInstallFailed": "{0} instal·lació fallida",
"NameSeasonNumber": "Temporada {0}", "NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconeguda", "NameSeasonUnknown": "Temporada desconeguda",
"NewVersionIsAvailable": "Hi ha disponible una versió nova del servidor de Jellyfin per a la descàrrega.", "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible", "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada", "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada",
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada", "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada", "NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
"NotificationOptionCameraImageUploaded": "Imatge de càmera pujada", "NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
@@ -54,8 +54,8 @@
"NotificationOptionPluginError": "Un complement ha fallat", "NotificationOptionPluginError": "Un complement ha fallat",
"NotificationOptionPluginInstalled": "Complement instal·lat", "NotificationOptionPluginInstalled": "Complement instal·lat",
"NotificationOptionPluginUninstalled": "Complement desinstal·lat", "NotificationOptionPluginUninstalled": "Complement desinstal·lat",
"NotificationOptionPluginUpdateInstalled": "Actualització del complement instal·lada", "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada",
"NotificationOptionServerRestartRequired": "El servidor s'ha de reiniciar", "NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
"NotificationOptionTaskFailed": "Tasca programada fallida", "NotificationOptionTaskFailed": "Tasca programada fallida",
"NotificationOptionUserLockedOut": "Usuari expulsat", "NotificationOptionUserLockedOut": "Usuari expulsat",
"NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada", "NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
@@ -64,79 +64,73 @@
"Playlists": "Llistes de reproducció", "Playlists": "Llistes de reproducció",
"Plugin": "Complement", "Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat", "PluginInstalledWithName": "{0} ha estat instal·lat",
"PluginUninstalledWithName": "S'ha instal·lat {0}", "PluginUninstalledWithName": "{0} ha estat desinstal·lat",
"PluginUpdatedWithName": "S'ha actualitzat {0}", "PluginUpdatedWithName": "{0} ha estat actualitzat",
"ProviderValue": "Proveïdor: {0}", "ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat", "ScheduledTaskFailedWithName": "{0} ha fallat",
"ScheduledTaskStartedWithName": "S'ha iniciat {0}", "ScheduledTaskStartedWithName": "{0} s'ha iniciat",
"ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}", "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
"Shows": "Sèries", "Shows": "Sèries",
"Songs": "Cançons", "Songs": "Cançons",
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.", "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}", "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
"Sync": "Sincronitza", "Sync": "Sincronitzar",
"System": "Sistema", "System": "Sistema",
"TvShows": "Sèries de TV", "TvShows": "Sèries de TV",
"User": "Usuari", "User": "Usuari",
"UserCreatedWithName": "S'ha creat l'usuari {0}", "UserCreatedWithName": "S'ha creat l'usuari {0}",
"UserDeletedWithName": "S'ha eliminat l'usuari {0}", "UserDeletedWithName": "L'usuari {0} ha estat eliminat",
"UserDownloadingItemWithValues": "{0} està descarregant {1}", "UserDownloadingItemWithValues": "{0} està descarregant {1}",
"UserLockedOutWithName": "S'ha expulsat a l'usuari {0}", "UserLockedOutWithName": "L'usuari {0} ha sigut expulsat",
"UserOfflineFromDevice": "{0} s'ha desconnectat de {1}", "UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
"UserOnlineFromDevice": "{0} està connectat des de {1}", "UserOnlineFromDevice": "{0} està connectat des de {1}",
"UserPasswordChangedWithName": "S'ha canviat la contrasenya per a l'usuari {0}", "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}",
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}", "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}", "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}", "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
"ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la mediateca", "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
"ValueSpecialEpisodeName": "Especial - {0}", "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versió {0}", "VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.", "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin", "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.", "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
"TaskRefreshChannels": "Actualitza els canals", "TaskRefreshChannels": "Actualitza els canals",
"TaskCleanTranscodeDescription": "Elimina els fitxers de transcodificacions que tinguin més d'un dia.", "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscode": "Neteja de les transcodificacions", "TaskCleanTranscode": "Neteja les transcodificacions",
"TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.", "TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.",
"TaskUpdatePlugins": "Actualització dels complements", "TaskUpdatePlugins": "Actualitza els connectors",
"TaskRefreshPeopleDescription": "Actualització de les metadades dels actors i directors de la mediateca.", "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.",
"TaskRefreshPeople": "Actualització de les persones", "TaskRefreshPeople": "Actualitza les persones",
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.", "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres", "TaskCleanLogs": "Neteja els registres",
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.", "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneig de les mediateques", "TaskRefreshLibrary": "Escaneja la biblioteca de mitjans",
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.", "TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols", "TaskRefreshChapterImages": "Extreure les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.", "TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
"TaskCleanCache": "Eliminació de la memòria cau", "TaskCleanCache": "Elimina arxius temporals",
"TasksChannelsCategory": "Canals per internet", "TasksChannelsCategory": "Canals d'internet",
"TasksApplicationCategory": "Aplicatiu", "TasksApplicationCategory": "Aplicació",
"TasksLibraryCategory": "Mediateca", "TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manteniment", "TasksMaintenanceCategory": "Manteniment",
"TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.", "TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
"TaskCleanActivityLog": "Buidatge del registre d'activitat", "TaskCleanActivityLog": "Buidar el registre d'activitat",
"Undefined": "Indefinit", "Undefined": "Indefinit",
"Forced": "Forçat", "Forced": "Forçat",
"Default": "Per defecte", "Default": "Per defecte",
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després descanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.", "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després descanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
"TaskOptimizeDatabase": "Optimització de la base de dades", "TaskOptimizeDatabase": "Optimitzar la base de dades",
"TaskKeyframeExtractorDescription": "Extracció de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.", "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
"TaskKeyframeExtractor": "Extracció de fotogrames clau", "TaskKeyframeExtractor": "Extractor de fotogrames clau",
"External": "Extern", "External": "Extern",
"HearingImpaired": "Discapacitat auditiva", "HearingImpaired": "Discapacitat auditiva",
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització", "TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
"TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.", "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.", "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
"TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció", "TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció",
"TaskAudioNormalization": "Estabilització de l'àudio", "TaskAudioNormalization": "Normalització dudio",
"TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.", "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio.",
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons", "TaskDownloadMissingLyricsDescription": "Baixar lletres de les cançons",
"TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin", "TaskDownloadMissingLyrics": "Baixar lletres que falten"
"TaskExtractMediaSegments": "Escaneig de segments multimèdia",
"TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
"TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització",
"TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.",
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari"
} }

View File

@@ -136,7 +136,5 @@
"TaskExtractMediaSegments": "Skenování segmentů médií", "TaskExtractMediaSegments": "Skenování segmentů médií",
"TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.", "TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.",
"TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay", "TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
"TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.", "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny."
"CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.",
"CleanupUserDataTask": "Pročistit uživatelská data"
} }

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