Compare commits

...

422 Commits

Author SHA1 Message Date
Jellyfin Release Bot 6c073e19dd Bump version to 12.0 2026-09-07 21:05:07 -04:00
Cody Robibero 71787c46d1 Merge pull request #17826 from joshuaboniface/fix/bump_version
Fix bump_version by removing Jellyfin.MediaEncoding.Keyframes
2026-09-07 21:03:02 -04:00
Joshua M. Boniface 35a391e99d Fix bump_version by removing Jellyfin.MediaEncoding.Keyframes
This file previously had the version in it, but it was removed in #17582
which caused this to fail. Remove it here.
2026-09-07 20:58:17 -04:00
Cody Robibero 85ebd83bdd Merge pull request #17821 from Shadowghost/more-counts
Expose optimized ItemCounts for byName items
2026-09-07 17:53:31 -04:00
Cody Robibero 882faf2b2d Merge pull request #17723 from jellyfin/renovate/bitfaster.caching-2.x
Update dependency BitFaster.Caching to 2.6.1
2026-09-07 17:44:24 -04:00
Cody Robibero d7d6cc44de Merge pull request #17824 from Raspberry-Monster/master
Fix Blu-ray multi-angle concat generation
2026-09-07 17:44:06 -04:00
Cody Robibero 42f7a6e720 Merge pull request #17820 from Shadowghost/only-download-missing-plugin-images
Only download missing plugin images
2026-09-07 17:43:57 -04:00
Cody Robibero 96f81b0c1b Merge pull request #17775 from slevin-7/fix-skia-sharpen-perf
Apply the resize sharpening kernel directly instead of via SKImageFilter
2026-09-07 17:43:48 -04:00
Cody Robibero 7d9bd1d3e4 Merge pull request #17816 from Shadowghost/version-aware-played-state
Share played state across alternate versions
2026-09-07 17:43:10 -04:00
Cody Robibero 2ffa74e7ea Merge pull request #17819 from Shadowghost/fix-tmdb-search
Fix TMDb search result ranking
2026-09-07 17:42:38 -04:00
krvi 837c34aee2 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-09-07 20:23:12 +00:00
renovate[bot] 0fc380b4a9 Update dependency BitFaster.Caching to 2.6.1 2026-09-07 19:46:42 +00:00
Shadowghost 2644163f57 Count distinct items for byName ItemCounts and batch every kind 2026-09-07 18:39:59 +02:00
krvi dfe57e484f Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-09-07 15:15:24 +00:00
Raspberry Kan 647c4ad090 Fix Blu-ray multi-angle concat generation
Filter Blu-ray playlist clips to AngleIndex == 0 when generating concat files to prevent clips from multiple angles from being played sequentially.
2026-09-07 22:53:22 +08:00
nextlooper42 6de9d1508f Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/
2026-09-07 12:22:59 +00:00
Anastasis Marinos b2eb088e0f Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/
2026-09-07 12:22:59 +00:00
Shadowghost e1e319cb5c Only download missing plugin images 2026-09-07 10:43:36 +02:00
Shadowghost 59cd5f983b Expose optimized ItemCounts for byName items 2026-09-07 10:30:01 +02:00
Shadowghost 04a7568902 Fix TMDb search result ranking 2026-09-07 08:23:31 +02:00
Cody Robibero 3e7d1158da Merge pull request #17794 from Shadowghost/plugin-install-fixes
Better handle timeouts on plugin operations
2026-09-06 12:47:45 -04:00
Shadowghost fd016cfb13 Share played state across alternate versions 2026-09-06 17:20:18 +02:00
Cody Robibero f898c35b96 Merge pull request #17791 from Shadowghost/fix-code-migration
Don't dispose application singletons after running a code migration
2026-09-06 07:45:43 -04:00
Cody Robibero bbb3a48963 Merge pull request #17810 from Shadowghost/fix-tmdb-recommendations
Fix TMDb recommendations
2026-09-06 07:37:18 -04:00
Cody Robibero 34feb3c04a Merge pull request #17763 from Shadowghost/fix-refresh-queue-and-directory-cache
Fix items being lost from the refresh queue and bound the directory service caches
2026-09-06 07:36:13 -04:00
Cody Robibero 8d108d2870 Merge pull request #17806 from jellyfin/fix/dovi-color-validation
Enforce dolby vision transfer check
2026-09-06 07:35:32 -04:00
Cody Robibero 2555ab7539 Merge pull request #17805 from Shadowghost/fix-unordered-multiepisode-nfo
Fix handling of unordered multi-episode NFOs
2026-09-06 07:34:45 -04:00
Cody Robibero edb6cd11da Merge branch 'master' into fix-code-migration 2026-09-06 07:34:30 -04:00
Cody Robibero a0a42c630e Merge pull request #17786 from Shadowghost/optimize-db
Optimize database after migrations
2026-09-06 07:33:07 -04:00
Shadowghost 63553803b1 Remove the unreachable null service provider path from code migrations 2026-09-06 09:49:39 +02:00
Shadowghost 7b638a22bb Add test 2026-09-06 09:07:22 +02:00
gnattu 011eed7622 fix dovi invalid doc 2026-09-06 14:50:30 +08:00
Shadowghost 344a6dcd2c Apply review suggestions 2026-09-06 08:38:43 +02:00
Shadowghost 79a6ac6d82 Don't read episode markers in episode titles as a multi-episode range 2026-09-06 08:04:34 +02:00
Shadowghost ca90347dc2 Resolve migration routine loggers from the application container 2026-09-06 07:54:11 +02:00
Cody Robibero 66d038c403 Merge pull request #17762 from Shadowghost/fix-scan-memory-leak
Bound change batches during a scan; keep ffprobe and image saves from failing
2026-09-06 01:28:02 -04:00
Shadowghost 2ac4dd9980 Fix TMDb recommendations 2026-09-06 06:42:53 +02:00
gnattu a1910a7b2e Enforce dolby vision transfer check
Dolby vision files having unexpected transfers now also marked as invalid, and it will now get its base video range from its base layer color transfer, as not all invalid dolby vision files are HDR now.
2026-09-06 02:53:52 +08:00
Shadowghost 9221e22498 Do not resolve bundled plugins via repos 2026-09-05 20:01:09 +02:00
Shadowghost 006809f02a Fix handling of unordered multi-episode NFOs 2026-09-05 19:32:56 +02:00
Shadowghost 93345f812e FIx naming 2026-09-05 18:49:27 +02:00
Cody Robibero 7c463f5fba Merge pull request #17674 from Oggeb1/BDMV-pgs
Fix PGS subtitles for BDMV with TrueHD
2026-09-05 12:16:25 -04:00
Shadowghost 74ce774eff Revert to doing after migration and on shutdown 2026-09-05 17:33:47 +02:00
Oggeb1 9528d0e601 Fix BDMV subtitles with external audio
Co-authored-by: gnattu <gnattu@users.noreply.github.com>
2026-09-05 17:15:00 +02:00
Cody Robibero 84589380e5 Merge pull request #17746 from kfarnung/fix/vobsub-idx-subtitle-language
Probe .idx instead of .sub for external VobSub subtitle language detection
2026-09-05 11:09:46 -04:00
Cody Robibero ffd61de4f3 Merge pull request #17799 from fmarcac/fix/syncplay-stale-session-requests
Drop SyncPlay requests from sessions that left the group
2026-09-05 11:07:56 -04:00
Cody Robibero 441a05ac88 Merge pull request #17768 from Shadowghost/fix-livetv-hls-audio-codec-and-manifest-direct-play
Fix Live TV HLS playback: bogus audio encoder and unplayable direct played manifests
2026-09-05 11:00:31 -04:00
Cody Robibero 44ecc909ff Merge pull request #17796 from fmarcac/fix/syncplay-shuffle-mode-crash
Fix crash when SyncPlay shuffle mode is set to sorted twice
2026-09-05 10:58:31 -04:00
Cody Robibero 2b0ccf2a30 Merge pull request #17800 from fmarcac/fix/syncplay-active-session-counter
Fix SyncPlay active session counter leaking on rejoin
2026-09-05 10:57:55 -04:00
Cody Robibero d4a23cd438 Merge pull request #17798 from fmarcac/fix/syncplay-ping-delay-units
Fix unit mismatch in the SyncPlay resume delay floor
2026-09-05 10:55:52 -04:00
fmarcac 7ce911a401 Clamp client reported ping in SyncPlay groups 2026-09-05 15:06:37 +02:00
fmarcac e356fe9146 Return the default ping for an empty SyncPlay group 2026-09-05 14:24:49 +02:00
fmarcac 0c05d9d1a9 Apply the SyncPlay resume delay floor in the correct unit 2026-09-05 14:24:49 +02:00
fmarcac e5bfe562bc Fix crash when SyncPlay shuffle mode is set to sorted twice 2026-09-05 14:24:48 +02:00
fmarcac 51a7d5d08a Fix SyncPlay active session counter leaking on rejoin 2026-09-05 14:24:48 +02:00
fmarcac 5acb200c02 Drop SyncPlay requests from sessions that left the group 2026-09-05 14:23:37 +02:00
Shadowghost f8470630be Retire runners that are cancelled before they start 2026-09-05 09:59:12 +02:00
Shadowghost bf5fb593e4 Do not treat timeouts as cancellations 2026-09-05 09:41:07 +02:00
Shadowghost 9c259027df Cleanup 2026-09-05 08:55:12 +02:00
Shadowghost c622a16261 Don't dispose application singletons after running a code migration 2026-09-05 07:37:09 +02:00
Shadowghost 5465e0c694 Optimize the database on startup instead of before shutdown 2026-09-05 07:36:17 +02:00
Shadowghost e5cd3381ac Limit cache size again
Co-Authored-By: Cody Robibero <cody@robibe.ro>
2026-09-05 07:09:29 +02:00
Cody Robibero c80f05fad1 Merge pull request #17767 from Shadowghost/post-scna-task-logging
Add comprehensive logging for post scan tasks
2026-09-04 21:13:54 -04:00
Shadowghost 46dd7d8e99 Invalidate the singleton DirectoryService cache on filesystem changes 2026-09-04 19:28:59 +02:00
Shadowghost 0d9c9c9ecc Fix the library scheduler never retiring its runners 2026-09-04 19:23:29 +02:00
Shadowghost 0b5bbb528a Optimize database after running migrations 2026-09-04 18:57:54 +02:00
krvi f10c84465a Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-09-04 16:54:10 +00:00
AlphaBeta 610887dc2c Translated using Weblate (Thai)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/th/
2026-09-04 15:42:44 +00:00
Shadowghost 5edb369e3b Apply review suggestion 2026-09-04 07:02:08 +02:00
Shadowghost fc37151fc4 Use platform separators in the DirectoryService path tests 2026-09-04 06:57:18 +02:00
Cody Robibero c96ff18e5b Merge pull request #17757 from felixfoertsch/fix/legacy-filter-tag-query
Fix UI stalls caused by large tag and genre sets
2026-09-03 18:36:37 -04:00
krvi 3f8315df4e Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-09-03 19:05:36 +00:00
Riccardo 1ccec11b91 Translated using Weblate (Bosnian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bs/
2026-09-03 13:37:37 +00:00
Riccardo d18d4f9cd2 Translated using Weblate (Luxembourgish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lb/
2026-09-03 13:37:36 +00:00
Riccardo 978552b1c5 Translated using Weblate (Irish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ga/
2026-09-03 13:37:35 +00:00
Riccardo f53bb0aae9 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-09-03 13:37:35 +00:00
Riccardo 24d8290462 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/
2026-09-03 13:37:34 +00:00
Riccardo 849437991a Translated using Weblate (Latvian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lv/
2026-09-03 13:37:34 +00:00
Riccardo 1b184b2b35 Translated using Weblate (Romanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ro/
2026-09-03 13:37:33 +00:00
Riccardo fb6e075e40 Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/
2026-09-03 13:37:33 +00:00
Riccardo 3682840d54 Translated using Weblate (Slovenian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sl/
2026-09-03 13:37:32 +00:00
Riccardo a6c0fe57c5 Translated using Weblate (Norwegian Bokmål)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nb_NO/
2026-09-03 13:37:31 +00:00
Riccardo f4d2da180d Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/
2026-09-03 13:37:30 +00:00
Riccardo b561d6aa59 Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/
2026-09-03 13:37:29 +00:00
Shadowghost b724e57458 Reduce comments 2026-09-03 11:51:02 +02:00
Cody Robibero 6e4d98e6db Merge pull request #17764 from Shadowghost/fix-tmdb-cache-retention
Bound the TMDb response cache so a library scan cannot fill it without limit
2026-09-02 18:00:32 -04:00
Cody Robibero 4f2f781e68 Merge pull request #17756 from Shadowghost/fix-people-creation
Fix people and artist validator creation and deletion handling
2026-09-02 18:00:22 -04:00
Albert 064de2172e Apply the resize sharpening kernel directly instead of via SKImageFilter
Since the SkiaSharp 3 update the MatrixConvolution image filter used in
SkiaEncoder.ResizeImage no longer has a fast CPU path: on the software
rasterizer it takes about 4.5 seconds per megapixel-sized image, which
turns every cold image request into a multi-second operation and makes
first-time loads of a library view take minutes.

Draw the resize without the paint filter and apply the identical 3x3
kernel (same weights, clamped edges, alpha included) directly on the
resized pixels instead. This drops a cold 1000x1500 -> 663x995 poster
render from ~4.6s to well under a second; the convolution pass itself
takes ~86ms. Output is visually unchanged.
2026-09-02 21:41:36 +02:00
Felix Förtsch c3a7de54f3 avoid correlated item value name queries 2026-09-02 11:33:54 +02:00
Felix Förtsch 5adafb446f test legacy item value filter semantics 2026-09-02 11:29:24 +02:00
Felix Förtsch 3df58ab775 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-02 11:05:36 +02:00
Shadowghost 8eb4964599 Never treat a manifest container as an audio codec or a direct play target 2026-09-02 09:29:48 +02:00
Shadowghost f0297ee9ce Add comprehensive logging for post scan tasks 2026-09-02 09:26:41 +02:00
Shadowghost d73e3d964e Optimize Caches
Co-Authored-By: Cody Robibero <cody@robibe.ro>
2026-09-02 07:06:08 +02:00
Cody Robibero b3766b00d4 Merge pull request #17761 from zerafachris/fix/fallback-font-404-missing
fix: return fallback gracefully when requested fallback font is missing
2026-09-01 20:45:05 -04:00
Shadowghost 3bb8611995 Bound the TMDb response cache so a library scan cannot fill it without limit 2026-09-01 23:04:33 +02:00
Shadowghost e5dc3b8a54 Bound the directory caches a singleton would otherwise hold for the process lifetime 2026-09-01 22:04:06 +02:00
Shadowghost 0e6c52f431 Guard the refresh queue so concurrent callers cannot lose the items they queue 2026-09-01 22:03:59 +02:00
Shadowghost ccdc69e3b0 Treat an item deleted mid-save as a no-op when saving its images 2026-09-01 21:42:07 +02:00
Shadowghost c56e14d8fb Keep a media process and its exit state usable by the caller that started it 2026-09-01 21:42:06 +02:00
Shadowghost c5f8a93513 Close a change batch on its own window so a library scan cannot grow it without bound 2026-09-01 21:42:06 +02:00
Vitalijus ea89b6ebc5 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-09-01 19:28:24 +00:00
Vitalijus 76418ec530 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-09-01 18:33:23 +00:00
Shadowghost 50f08d41a4 Find dead people and artists by id, not by name 2026-09-01 19:49:25 +02:00
zerafachris 20678c9a8c fix: return fallback gracefully when requested fallback font is missing
GetFallbackFont() called .First() on the font file sequence, which throws
System.InvalidOperationException when no file matched the requested name,
causing HTTP 500. Change to .FirstOrDefault() so a missing font falls
through to the existing null guard and returns HTTP 200 OK (the empty
response is intentional to avoid breaking SubtitlesOctopus).

Fixes #17683.

Prepared with AI assistance (Claude Code, Anthropic), reviewed for correctness before submission.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-01 16:25:59 +02:00
Felix Förtsch 145258865a avoid correlated legacy item value filter queries
The legacy filter queries projected ItemValue before grouping tags and genres. EF Core translated that shape into duplicated correlated aggregates, causing multi-minute requests for libraries with many distinct values and blocking unrelated SQLite-backed API calls.

Join ItemValuesMap directly to ItemValues before grouping. This preserves type and item filtering, clean-value grouping, minimum-value selection and ordering while producing one aggregate query. Add an in-memory SQLite regression test for result semantics and both SQL shapes.
2026-09-01 14:37:44 +02:00
Shadowghost 792ce4a391 Fix people validator not creating missing people 2026-09-01 07:36:53 +02:00
Cody Robibero 4910aafa1a Merge pull request #17743 from Shadowghost/migration-logging
Add progress logging to data migrations
2026-08-31 17:49:14 -04:00
Cody Robibero 726ae93a92 Merge pull request #17750 from Shadowghost/refresh-people-ordering
Enforce reliable processing order in PeopleValidationTask
2026-08-31 17:48:21 -04:00
Cody Robibero 0cec5661e0 Merge pull request #17752 from Shadowghost/fix-linkedchildren-recursive
Fix recursive handling for LinkedChildren
2026-08-31 17:47:33 -04:00
Cody Robibero fe73b1a949 Merge pull request #17748 from Shadowghost/fix-default-config-12.x
Fix ListenBrainz settings and similar item defaults
2026-08-31 17:46:30 -04:00
Cody Robibero 80324b19fb Merge pull request #17724 from Shadowghost/fix-by-name-item-counts
Fix item counts on the by-name endpoints
2026-08-31 15:19:08 -04:00
Shadowghost 05844d60c1 Apply review suggestions, remove Logo on config page 2026-08-31 20:44:41 +02:00
Cody Robibero eafe5ba6ed Merge pull request #17729 from Shadowghost/enforce-acl-similar-items
Enforce permissions on similar items
2026-08-31 12:02:33 -04:00
Shadowghost 7ccce8e0e7 Fix recursive handling for LinkedChildren 2026-08-31 17:56:56 +02:00
Shadowghost 40ab2c4284 Enforce reliable processing order in PeopleValidationTask 2026-08-31 15:44:32 +02:00
Shadowghost d1dda7f6c5 Add progress logging to data migrations 2026-08-31 10:39:39 +02:00
Shadowghost ff36560575 Enable local similarity providers on libraries upgraded from 10.11 2026-08-31 08:37:57 +02:00
Shadowghost e9abaa519c Repair the blank algorithm select and missing logo in ListenBrainz plugin settings 2026-08-31 08:37:44 +02:00
Shadowghost 359e8069d0 Default the similar items TMDb cache to 90 days 2026-08-31 08:37:35 +02:00
Kyle Farnung 39c2885fd6 Probe .idx instead of .sub for external VobSub subtitle language detection
External VobSub subtitle pairs (.idx and .sub) were only probed via the
bare .sub file. In cases where multiple languages are present, this
results in missing language metadata.

Fix by detecting the matching .idx file during media info resolution
to run ffprobe on that file and skip processing the .sub entirely.
ffprobe will automatically find the matching (same directory,
case-sensitive base) .sub file and process both.

Added regression tests covering idx/sub pairing, unpaired files,
cross-directory pairs, and language-flagged filenames.

Fixes #17745
2026-08-30 17:29:49 -07:00
Cody Robibero 7d6633ad1e Merge pull request #17744 from Shadowghost/fix-master
Fix formatting and test
2026-08-30 17:52:39 -04:00
Shadowghost 11adad0e67 Fix formatting and test 2026-08-30 22:29:36 +02:00
Cody Robibero 420d44f638 Merge commit from fork
Fix broken access control in session management
2026-08-30 14:40:25 -04:00
Cody Robibero 9fe5a53e47 Merge commit from fork
Secure library paths
2026-08-30 14:40:16 -04:00
Cody Robibero 2996f726c1 Merge commit from fork
Prevent SSRF, local file disclosure and DoS via external references in SVG rendering
2026-08-30 14:40:03 -04:00
Cody Robibero 37db4bda53 Merge commit from fork
Fix calling user for playlist items
2026-08-30 14:39:28 -04:00
Cody Robibero 0d78e44633 Merge pull request #17739 from Shadowghost/simplify-search-query
Filter search candidates by user access in a single query
2026-08-30 13:29:59 -04:00
Cody Robibero 4962e4ec33 Merge pull request #17735 from Shadowghost/fix-season-child-count
Count a season's episodes by the season they belong to
2026-08-30 13:29:13 -04:00
Shadowghost 07e97c0ee9 Filter search candidates by user access in a single query 2026-08-30 12:33:12 +02:00
krvi 24288e79a9 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-30 00:59:15 +00:00
Shadowghost 27d898e59e Count a season's episodes by the season they belong to 2026-08-29 21:39:14 +02:00
Shadowghost 95281a2205 Revert "Fix ParentId for episodes in virtual seasons"
This reverts commit 6da85a0aaa87bdd2c81ef2b1936a2e35c1478b76.
2026-08-29 21:36:49 +02:00
Cody Robibero 5a2f45ab4b Merge pull request #17731 from Shadowghost/anime-id-season-folder
Support anime provider ids on season folders
2026-08-29 09:57:26 -04:00
Oggeb1 4207e89a7d Merge branch 'jellyfin:master' into BDMV-pgs 2026-08-29 14:02:22 +02:00
Oskar Bali 163895b99f Fix BDMV with external subtitle
GetSubtitleStreamIndexForFfmpeg treated
external and internal subtitles the same. This
made subtitles out of sync with the video.
2026-08-29 13:52:20 +02:00
krvi fbb0f1afbc Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-28 23:35:45 +00:00
Shadowghost ea5f83328d Support anime provider ids on season folders 2026-08-28 15:48:17 +02:00
Shadowghost a45e66d43c Enforce permissions on similar items 2026-08-28 11:01:43 +02:00
Pavel Miniutka 6ad1e341b1 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/
2026-08-28 07:44:55 +00:00
Pavel Miniutka b46e66627c Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/
2026-08-28 07:30:44 +00:00
Pavel Miniutka 7116d3bb72 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/
2026-08-28 07:26:34 +00:00
Shadowghost 99f21f1662 Apply review suggestions 2026-08-28 07:21:37 +02:00
Cody Robibero 9126de26c0 Merge pull request #17719 from theguymadmax/fix-episodes-ParentId
Fix ParentId for episodes in virtual seasons
2026-08-27 19:10:43 -04:00
Cody Robibero 75df54611a Merge pull request #17718 from Shadowghost/fix-virtual-children
Fix children count on virtual items
2026-08-27 19:10:33 -04:00
Shadowghost ceeeaaab8e Fix By-Name item count handling 2026-08-27 10:19:36 +02:00
theguymadmax 6da85a0aaa Fix ParentId for episodes in virtual seasons 2026-08-26 21:51:31 -04:00
Shadowghost 2aad6047c8 Fix children count on virtual items 2026-08-26 18:57:50 +02:00
Dan Bishop 1cc490fb19 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en_GB/
2026-08-26 13:52:43 +00:00
Cody Robibero 904699ba21 Merge pull request #17709 from Shadowghost/fix-people-task-perist
Persist the refresh stamp so the people task stops redoing its work
2026-08-25 19:10:44 -04:00
Cody Robibero f682c22b08 Merge pull request #17715 from Shadowghost/fix-people-cleanup
Delete credits nothing maps to and bound item-by-name folder names
2026-08-25 18:28:52 -04:00
Cody Robibero a68793d08a Merge pull request #17716 from Shadowghost/tmdb-aggregate-cast
Build a TMDb series cast from the aggregated credits
2026-08-25 18:26:50 -04:00
Cody Robibero e79e2cba27 Merge pull request #17714 from Shadowghost/fix-localized-view-ids
Stop deriving user view ids from their localized name
2026-08-25 18:26:09 -04:00
Cody Robibero f3298eee69 Merge pull request #17713 from LTe/fix/sort-isplayed-series
Fix IsPlayed and IsUnplayed sorting for shows and collections
2026-08-25 18:24:46 -04:00
Cody Robibero 88df882061 Merge pull request #17710 from Shadowghost/fix-omdb-people
Fix OMDB People handling
2026-08-25 18:23:42 -04:00
Cody Robibero 118940fff8 Merge pull request #17711 from Shadowghost/fix-image-logging
Say which image and item failed instead of logging a blank path
2026-08-25 18:22:02 -04:00
Shadowghost 79c37bfcd5 Build a TMDb series cast from the aggregated credits 2026-08-25 21:06:51 +02:00
Shadowghost a81b90f570 Fix tests 2026-08-25 21:03:51 +02:00
Shadowghost 1d24c1df17 Keep an OMDb credit whole when its annotation holds a comma 2026-08-25 20:43:44 +02:00
Shadowghost 4147a83b93 Handle generational suffixes 2026-08-25 20:42:41 +02:00
Shadowghost 6978dfc294 Delete a credit once no item maps to it any more 2026-08-25 20:31:30 +02:00
Shadowghost 8c0775e941 Bind the folder name an item-by-name entity resolves to 2026-08-25 20:31:09 +02:00
Piotr Niełacny 5e621d0e3f Order IsPlayed and IsUnplayed by the played state the filter reports
Ordering mapped both keys to the item's own stored UserData row. Folders do not
have one: a series, season or box set counts as played when no descendant is
left unplayed, which is what the isPlayed filter and the DTO both report. A
mixed library therefore sorted every series and box set into the unplayed group,
and a query could filter and sort by two different notions of "played".

Extract the filter's predicate into BuildIsPlayedFilter and route both sort keys
through it so the two cannot drift apart again.
2026-08-25 15:19:38 +02:00
Shadowghost cefa78fc1d Prevent SSRF, local file disclosure and DoS via external references in SVG rendering 2026-08-25 11:04:24 +02:00
Shadowghost 0c560b22ce Secure library paths 2026-08-25 09:28:45 +02:00
Shadowghost 38093e2952 Fix test 2026-08-24 22:27:11 +02:00
Shadowghost 3fafdbc281 Say which image and item failed instead of logging a blank path 2026-08-24 22:12:57 +02:00
Shadowghost 9cc47c4fd6 Persist the refresh stamp so the people task stops redoing its work 2026-08-24 21:57:14 +02:00
Shadowghost 4c524f033f Fix OMDB People handling 2026-08-24 21:56:31 +02:00
Bond-009 422b2bb3d9 Update dependency UTF.Unknown to 2.7.0 (#17697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 19:07:33 +02:00
Shadowghost 911ac3769c Fix GHSA-9x85-gx46-6522 2026-08-23 19:20:58 -04:00
Vitalijus 971be1b658 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-23 21:54:28 +00:00
Cody Robibero 1cc698a30b Merge pull request #17702 from Shadowghost/fix-person-metadata-refresh
Fix person metadata not being fetched on demand or by the people task
2026-08-23 17:38:19 -04:00
Shadowghost 4cd24a19d4 Use FullRefresh 2026-08-23 14:35:13 +02:00
Cody Robibero af8e19b169 Merge pull request #17693 from Shadowghost/fix-series-merging
Fix series merging leaking across libraries and under-counting merged children
2026-08-23 08:20:29 -04:00
Cody Robibero 75ae31895f Merge pull request #17619 from Shadowghost/fix-metadata-language-fallback
Fix English metadata blocking localized providers ranked below it
2026-08-23 08:20:20 -04:00
Karan Singh BHardwaj 484291b0c1 Translated using Weblate (Hindi)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/
2026-08-23 11:23:04 +00:00
chiphead2332 e8927bc300 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/
2026-08-23 11:23:03 +00:00
DeaDvey 7758beba99 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en_GB/
2026-08-23 09:45:51 +00:00
Shadowghost 090b610eb1 Fix person metadata not being fetched on demand or by the people task 2026-08-23 09:27:26 +02:00
Koralski 19235909fe Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bg/
2026-08-23 07:26:27 +00:00
krvi c3ed1407ca Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-22 17:14:15 +00:00
krvi 0b7b53a9fd Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-22 16:11:17 +00:00
Shadowghost 284c6f00db Merge remote-tracking branch 'upstream/master' into fix-metadata-language-fallback 2026-08-22 17:31:04 +02:00
Shadowghost ea2c794cb2 Adapt to master 2026-08-22 17:22:41 +02:00
Shadowghost e1af8dce05 Merge remote-tracking branch 'upstream/master' into fix-series-merging 2026-08-22 17:18:01 +02:00
Cody Robibero 0d02638e82 Merge pull request #17700 from Shadowghost/better-people-resolution
Look up people by item via the credit map instead of a full scan
2026-08-22 08:50:42 -04:00
Cody Robibero 682c45090c Merge pull request #17698 from Shadowghost/unbreak-breakOnNonKeyFrames-dlna
Mark breakOnNonKeyFrames as XMLIgnore
2026-08-22 08:50:00 -04:00
Cody Robibero b2ba726635 Merge pull request #17691 from Shadowghost/fix-top-parent-fallback
Fall back to the ancestor filter when a view has no top parents
2026-08-22 08:49:39 -04:00
Cody Robibero 49112163eb Merge pull request #17584 from Shadowghost/safeguard-invalid-provider-ids
Safeguard against invalid provider ids
2026-08-22 08:49:07 -04:00
Cody Robibero 0d4cbb999b Merge pull request #17607 from Shadowghost/optimize-db-helper-memory
Optimize query helper memory
2026-08-22 08:45:42 -04:00
Cody Robibero c04f11bd76 Merge pull request #17685 from jellyfin/GHSA-wwwm-px48-fpvq-v12
Fix GHSA-wwwm-px48-fpvq
2026-08-22 08:43:07 -04:00
Cody Robibero 6617a27de3 Merge pull request #17682 from theguymadmax/hdhomerun-directplay
Allow direct play for HDHomeRun Live TV tuners
2026-08-22 08:42:57 -04:00
Cody Robibero 4477f41857 Merge pull request #17678 from theguymadmax/fix-mixed-latest-items
Fix latest items for mixed libraries
2026-08-22 08:42:38 -04:00
Shadowghost 20b4a59281 Look up people by item via the credit map instead of a full scan 2026-08-22 11:22:31 +02:00
Shadowghost 42c70fba63 Additional fixes
Co-Authored-By: Cody Robibero <cody@robibe.ro>
2026-08-22 08:50:14 +02:00
Shadowghost 8e8cf57025 Mark breakOnNonKeyFrames as XMLIgnore 2026-08-22 07:43:48 +02:00
Shadowghost 587f06dccc Multiple fixes and improvements
Co-Authored-By: Cody Robibero <cody@robibe.ro>
2026-08-22 07:42:56 +02:00
Shadowghost 7ad5ff6aa7 Merge remote-tracking branch 'upstream/master' into optimize-db-helper-memory
# Conflicts:
#	src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
2026-08-22 07:01:58 +02:00
renovate[bot] a5880c8622 Update dependency UTF.Unknown to 2.7.0 2026-08-22 03:30:29 +00:00
Shadowghost 335d97d868 Use WhereOneOrMany 2026-08-21 22:59:39 +02:00
Shadowghost 8e80677bdd Fix series merging leaking across libraries and under-counting merged children 2026-08-21 22:48:47 +02:00
Shadowghost 4df580f0e8 Fall back to the ancestor filter when a view has no top parents 2026-08-21 20:40:52 +02:00
Joel Sprouse 9fa0533506 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en_GB/
2026-08-21 17:18:51 +00:00
Cody Robibero 9ba91d4583 Normalize fix, apply in more places 2026-08-20 18:10:50 -04:00
Shadowghost a8da0664a3 Fix GHSA-wwwm-px48-fpvq
# Conflicts:
#	MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
2026-08-20 17:51:42 -04:00
Gabriel Popa d6bcad3b59 Translated using Weblate (Romanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ro/
2026-08-20 21:36:09 +00:00
theguymadmax 40fbe4a771 Allow direct play for HDHomeRun tuners 2026-08-20 15:25:37 -04:00
therealhampus 678975fbd7 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sv/
2026-08-20 16:35:33 +00:00
Tim Eisele fb50b4df8b Stop user updates from orphaning permission and preference rows (#17645)
* Stop user updates from orphaning permission and preference rows

* Make UserId non-nullable

* Remove unnecessary ToList

* Update Jellyfin.Server.Implementations/Users/UserManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

---------

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2026-08-20 18:35:26 +02:00
Oggeb1 c15656280e Merge branch 'jellyfin:master' into BDMV-pgs 2026-08-20 10:56:35 +02:00
Jacky He c70933a23f Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/
2026-08-20 02:51:23 +00:00
Jacky He ad9056c8fc Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2026-08-20 02:51:22 +00:00
Jacky He 77988e51b6 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/
2026-08-20 02:51:21 +00:00
theguymadmax 80fe8c67e9 Fix latest items for mixed libraries 2026-08-19 18:34:21 -04:00
Blackspirits 4029de92a1 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2026-08-19 22:08:08 +00:00
krvi 19d919f8be Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-19 16:33:14 +00:00
Blackspirits ef01d88912 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-19 16:33:14 +00:00
Shadowghost bd085665d6 Merge remote-tracking branch 'upstream/master' into safeguard-invalid-provider-ids 2026-08-19 16:13:29 +02:00
Ömür Ege Kiraz f586fc063f Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2026-08-19 13:29:15 +00:00
Oskar Bali 0b20d7a05b Fix BDMV PGS subtitles with TrueHD
Add myself CONTRIBUTORS.md
2026-08-19 12:19:32 +02:00
MakeMike 17221057bc Translated using Weblate (Armenian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hy/
2026-08-19 08:00:16 +00:00
Cody Robibero 04b77ca801 Merge pull request #17670 from Shadowghost/harden-musicbrainz
More resilient MusicBrainz lookup
2026-08-18 19:28:22 -04:00
Blackspirits b11734d955 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-18 22:07:07 +00:00
Cody Robibero 4a659fb30c Merge pull request #17631 from itsb/fix/confirmed-resume-position-master
Use client-reported position for idle playback cleanup
2026-08-18 17:12:00 -04:00
MoonsvnLyn c203da245d Fix Cheesegeezer GitHub URL typo in CONTRIBUTORS (#17659) 2026-08-18 22:36:32 +02:00
deepak r 0f36c4a200 Translated using Weblate (Tamil)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ta/
2026-08-18 18:36:57 +00:00
Jonathan Biton c4dec76b9c Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/
2026-08-18 18:36:57 +00:00
m0g3r 46be43ad24 Prevent orphaned user permissions and preferences (#17643)
Prevent orphaned user permissions and preferences
2026-08-18 18:16:48 +02:00
Bond-009 3fe35fcbc1 Merge pull request #17665 from jellyfin/renovate/microsoft
Update Microsoft to 5.9.0
2026-08-18 17:29:29 +02:00
Shadowghost 43cdbe856a More resilient MusicBrainz lookup 2026-08-18 11:07:31 +02:00
Cody Robibero 72405bb146 Merge pull request #17658 from martin-77/fix/child-count-sql-variable-limit
Fix SQLite variable limit in child count batches
2026-08-17 18:18:29 -04:00
renovate[bot] 2bb281ad4c Update Microsoft to 5.9.0 2026-08-17 19:07:58 +00:00
aleksantero a2ab2fe340 Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/
2026-08-17 17:56:58 +00:00
Blackspirits 7df3c09760 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2026-08-17 08:10:11 +00:00
Blackspirits d26d3f4967 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-17 08:10:10 +00:00
Blackspirits 8718c00a5d Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2026-08-17 07:55:04 +00:00
Blackspirits 0c68b190bc Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-17 07:55:04 +00:00
Thadah D. Denyse 2c26ae6724 Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eu/
2026-08-17 07:49:17 +00:00
Emanuel Lopes 300c277816 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-17 07:49:17 +00:00
nenadsuperzmaj 69b21ddc0e Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sr/
2026-08-17 07:16:19 +00:00
Gallyam Biktashev 90a0e9d962 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/
2026-08-17 07:16:18 +00:00
martin-77 c539ee7e81 Fix SQLite variable limit in child count batches 2026-08-17 01:43:10 +02:00
Cody Robibero f97b7b120d Merge pull request #17655 from martin-77/fix/playlist-sql-variable-limits
Fix large playlist persistence with WhereOneOrMany
2026-08-16 18:53:43 -04:00
martin-77 7c148c3c7c Fix large playlist persistence 2026-08-16 21:30:37 +02:00
Bond-009 4fe980f25b Merge pull request #17614 from jellyfin/renovate/dotnet-monorepo
Update dependency dotnet-ef to v10.0.11
2026-08-16 16:51:32 +02:00
itsb f0c5370e27 Preserve unknown idle playback position 2026-08-16 09:09:18 -05:00
Andrea Baitelli 5f14c12c89 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-08-16 13:18:01 +00:00
Andrea Baitelli 49c78d4624 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-08-16 13:13:05 +00:00
Pedro Fonseca 1b698db422 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-16 12:59:23 +00:00
Bond-009 97d3e2cba6 Merge pull request #17638 from jellyfin/renovate/microsoft
Update dependency Microsoft.NET.Test.Sdk to 18.9.0
2026-08-16 14:19:31 +02:00
Bond-009 4680d00d2e Merge pull request #17630 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.37.7
2026-08-16 13:48:16 +02:00
Ulrik 30c2603911 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/
2026-08-16 04:22:54 +00:00
Vitalijus a74246a000 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-15 20:59:10 +00:00
KecskeTech 1e04187b52 Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/
2026-08-15 16:36:20 +00:00
itsb 499e64b1c3 Use client-reported position for idle playback cleanup 2026-08-14 23:31:39 -05:00
krvi 916c3c9cc3 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-15 01:00:08 +00:00
VeryUsual f70e0563ee Translated using Weblate (Esperanto)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eo/
2026-08-15 01:00:07 +00:00
AlaronAndes d2f517a858 Translated using Weblate (Albanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sq/
2026-08-15 01:00:07 +00:00
dodog 0ae4a70e1c Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/
2026-08-15 01:00:06 +00:00
Shadowghost 4de43d36dd Require session ownership for additional users, capabilities and viewing reports 2026-08-14 14:50:07 +02:00
Shadowghost b1e3cf1341 Key active sessions by user to prevent session takeover 2026-08-14 14:50:06 +02:00
Shadowghost dd7de41878 Enforce EnableRemoteControlOfOtherUsers on session control 2026-08-14 14:50:06 +02:00
Cody Robibero e80fc120bf Merge pull request #17636 from st7105/fix/clean-web-dl-release-tag
Recognize WEB-DL release tags in video names
2026-08-14 08:29:37 -04:00
Cody Robibero 6982ba99ce Merge branch 'master' into fix/clean-web-dl-release-tag 2026-08-14 07:11:04 -04:00
renovate[bot] 8cb7297e27 Update dependency Microsoft.NET.Test.Sdk to 18.9.0 2026-08-14 11:07:01 +00:00
renovate[bot] 95d95de6f1 Update github/codeql-action action to v4.37.7 2026-08-14 11:06:54 +00:00
renovate[bot] 7704346e9c Update dependency dotnet-ef to v10.0.11 2026-08-14 11:06:49 +00:00
Cody Robibero 6a021d5dae Merge pull request #17637 from IDisposable/fix/de-localization-test-error
Fix other two unit test for localization of Artists in DE
2026-08-14 07:05:50 -04:00
José Tojeiro 6ebe66c19d Translated using Weblate (Galician)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gl/
2026-08-14 10:02:59 +00:00
Shadowghost 40a449c6f2 Count alternate versions in the media filters and align filter conditions 2026-08-14 08:46:21 +02:00
Shadowghost 06e37a7bf6 Merge remote-tracking branch 'upstream/master' into optimize-db-helper-memory 2026-08-14 08:09:38 +02:00
Shadowghost c77649d21e Skip alternate version links when resolving link parents 2026-08-14 07:39:49 +02:00
Vitalijus 8f97e690c8 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-14 04:01:38 +00:00
Marc Brooks b557dcfa45 Fix other two missing DE localization DE updates
Turns out there were two more instances of test broken by commit 21fec95b07
2026-08-13 22:31:42 -05:00
st7105 7b66b32cdf Recognize WEB-DL release tags in video names 2026-08-14 06:03:42 +03:00
Cody Robibero 7f3bbd5654 Merge pull request #17633 from IDisposable/chore/fix-de-localization-unit-test
Fix unit test for localization of Artists in DE
2026-08-13 18:37:14 -04:00
Marc Brooks a7c0c92019 Fix unit test for localization
DE localization for Artists was changed in localization commit 21fec95b07
2026-08-13 17:11:31 -05:00
Cody Robibero e1a619b79e Merge pull request #17615 from jellyfin/renovate/microsoft
Update Microsoft to 10.0.11
2026-08-13 16:27:06 -04:00
Cody Robibero 12226d2212 Merge pull request #17604 from theguymadmax/fix-FindArtists
Fix FindArtists
2026-08-13 16:26:48 -04:00
ZeroAiden ae8723026d Translated using Weblate (Malay)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ms/
2026-08-13 04:04:40 +00:00
Vitalijus a730d8a7ba Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-12 23:27:15 +00:00
Psychotrickser 21fec95b07 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/
2026-08-12 12:00:21 +00:00
Shadowghost 7e6709f023 Fix formatting 2026-08-12 08:48:42 +02:00
Shadowghost a4ff630d1f Fix English metadata blocking localized providers ranked below it 2026-08-12 08:39:00 +02:00
Shadowghost 9a07d4336b Make the media stream filter index covering 2026-08-12 08:16:09 +02:00
Shadowghost 2b5625d1c4 Resolve link owners from LinkedChildren 2026-08-12 08:16:03 +02:00
Shadowghost 9ae6ffe441 Share the SQLite fixture across the item tests 2026-08-12 08:15:55 +02:00
renovate[bot] 36fccdf6d4 Update Microsoft to 10.0.11 2026-08-12 04:43:32 +00:00
Bond-009 4e2fc33a61 Merge pull request #17551 from jellyfin/renovate/z440.atl.core-7.x
Update dependency z440.atl.core to 7.16.0
2026-08-11 18:17:55 +02:00
Shadowghost fa7fdf5884 Optimize query helper memory 2026-08-11 18:09:23 +02:00
renovate[bot] 2deb97bac3 Update dependency z440.atl.core to 7.16.0 2026-08-11 14:01:27 +00:00
theguymadmax 9220c2bd63 Fix FindArtists 2026-08-11 09:40:11 -04:00
Cody Robibero 6d501ba418 Merge pull request #17570 from Shadowghost/fix-ef-drift
Fix EF core designer drifts
2026-08-10 19:13:29 -04:00
Cody Robibero 2a8d168796 Merge pull request #17579 from Shadowghost/fix-removal-notification
Fix missing ItemRemoved events and search fallback after access filtering
2026-08-10 18:28:22 -04:00
Cody Robibero 34cc90f5e2 Merge pull request #17599 from Shadowghost/unbreak-master
Fix master build
2026-08-10 17:29:12 -04:00
Shadowghost 4e9713a032 Apply review suggestions 2026-08-10 23:10:33 +02:00
Shadowghost f82101332d Fix master build 2026-08-10 23:04:01 +02:00
Shadowghost 7b6ae06f3b Merge remote-tracking branch 'upstream/master' into safeguard-invalid-provider-ids 2026-08-10 23:03:52 +02:00
Shadowghost 26fbd1eac0 Fix master build 2026-08-10 22:46:31 +02:00
Cody Robibero 35e86416af Merge pull request #17576 from obiwantoby/perf/batch-mediasourcecount-dto
Bugfix: #17547 | Batching MediaSourceCount into one call
2026-08-10 16:15:54 -04:00
Cody Robibero c210ea87f2 Merge pull request #17597 from theguymadmax/fix-people-filtering
Fix PersonTypes not applied when filtering by person
2026-08-10 16:14:19 -04:00
Cody Robibero 4a1fcca182 Merge pull request #17582 from Shadowghost/fix-assemblies
Fix assemblies
2026-08-10 16:12:27 -04:00
Cody Robibero f5211a6722 Merge pull request #17590 from TOomaAh/fix/limit-remote-fetch-similar
fix: bound remote provider pagination
2026-08-10 16:12:11 -04:00
Cody Robibero 0b77a520fa Merge pull request #17588 from TOomaAh/fix/is-airing-filter
fix: correct IsAiring negation to exclude airing items
2026-08-10 16:11:30 -04:00
Cody Robibero d0e0e291f2 Merge pull request #17569 from vavallee/fix/17056-no-image-upscaling
Stop image endpoints from upscaling beyond the source resolution
2026-08-10 16:10:17 -04:00
Cody Robibero 82ecfde2f7 Update MediaBrowser.Controller/Drawing/ImageHelper.cs
Co-authored-by: Tim Eisele <Tim_Eisele@web.de>
2026-08-10 16:10:08 -04:00
Cody Robibero 7be16e9f74 Merge pull request #17563 from Shadowghost/cleanup-sql-helpers
Cleanup and simplify query helpers
2026-08-10 16:10:00 -04:00
theguymadmax f441c5f125 Fix PersonTypes not applied when filtering by person 2026-08-10 11:03:14 -04:00
hoanghuy309 e6e3099da9 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/vi/
2026-08-10 10:46:24 +00:00
Kiki d7dd98858e Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2026-08-10 10:46:24 +00:00
Dan Tsivinsky 3d3d3c26b8 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/
2026-08-10 10:46:23 +00:00
TOomaAh 5f3e938d09 fix: bound remote provider pagination 2026-08-09 17:18:54 +02:00
TOomaAh 3031aa91ba fix: correct IsAiring negation to exclude airing items 2026-08-09 16:28:20 +02:00
Vitalijus fb763c47bf Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-08 21:55:48 +00:00
Doougfoks a65b614449 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/
2026-08-08 20:28:52 +00:00
brandon 505a6ebf8f Use WhereOneOrMany helper for the alternate version id filter
Filter the parent ids with the WhereOneOrMany query helper instead of a
raw Contains so the id list is wrapped in EF.Parameter and EF Core reuses
one compiled query plan across calls, matching how the rest of the item
queries build their id filters.
2026-08-08 15:20:39 -04:00
Shadowghost 8c4dfc0b71 Safeguard against invalid provider ids 2026-08-08 19:49:26 +02:00
brandon 10d108a1f4 Address review on MediaSourceCount batching
Rename GetItemsWithAlternateVersions to GetItemIdsWithAlternateVersions
across the interfaces and implementations since it returns ids. Return
the hashset straight from the query instead of materializing an array
first. Rename the DtoService guard to mayHaveAlternateVersions and
invert it so the computed path is the explicit case. Assert the media
source count value in the batch skip test and add a test covering an
item that is in the returned set still resolving to the correct count.
2026-08-08 12:33:11 -04:00
Shadowghost 428c0f19bc Fix bump_version script 2026-08-08 18:26:46 +02:00
Shadowghost afd91a3d9d Fix assemblies 2026-08-08 18:20:19 +02:00
queeup a0b291a463 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2026-08-08 16:14:51 +00:00
Thadah D. Denyse a9e4c826aa Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eu/
2026-08-08 14:03:01 +00:00
hoanghuy309 b189813eb1 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/vi/
2026-08-08 14:02:57 +00:00
queeup 7c20ea2708 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2026-08-08 14:02:53 +00:00
Shadowghost e63c05137a Fix missing ItemRemoved events and search fallback after access filtering 2026-08-08 10:59:37 +02:00
brandon c091ffdc6b Batch alternate version detection in DtoService to remove MediaSourceCount N+1
Browsing a page of videos with the MediaSourceCount field ran one alternate
version query per item, each opening a fresh DbContext. On a large library that
turned a single page into hundreds of sequential round trips and made the Items
endpoint take tens of seconds while holding a request thread the whole time.

Detect which videos own alternate versions once per page with a single query,
mirroring the existing people batch. Videos absent from that set have a single
media source, so the per item lookups are skipped for the common case. Behavior
is unchanged: a video with no alternates already resolved to a count of one.

Adds a regression test asserting the count resolves from the batch and the per
item lookups are never called.
2026-08-07 22:51:45 -04:00
Cody Robibero 70ffd25b50 Merge pull request #17560 from IDisposable/chore/pessimistic-reality-check
Add warning that PessimisticLockBehavior is unsafe
2026-08-07 21:43:40 -04:00
Cody Robibero 247ee406a9 Merge pull request #17571 from obiwantoby/perf/batch-people-dto
Batch people lookups when building item DTOs
2026-08-07 21:43:21 -04:00
Cody Robibero 2218f2931c Merge pull request #17492 from GOvEy1nw/fix/image-cache-overlay-key
fix(images): disambiguate progress overlay cache keys
2026-08-07 21:42:16 -04:00
Cody Robibero 6bc1c18004 Merge pull request #17541 from vdatanet/fix/byname-total-record-count
Fix by-name endpoints reporting TotalRecordCount=0 next to a populated Items array
2026-08-07 21:40:58 -04:00
Cody Robibero eee818094e Merge pull request #17525 from rlauuzo/master
Clear metadata provider cache when provider parts are registered
2026-08-07 21:40:22 -04:00
Cody Robibero 0ec7cc5f5b Merge pull request #17555 from IDisposable/fix/reorder-update-items
Delete old related info in bulk as late as possible in UpdateOrInsertItems
2026-08-07 21:40:11 -04:00
Cody Robibero 871970120d Merge pull request #17521 from Shadowghost/fix-plugin-disable
Fix disabled plugins being re-enabled on restart
2026-08-07 21:39:39 -04:00
Cody Robibero 59bef7940b Merge pull request #17559 from IDisposable/fix/optimistic-lock-behavior
Fix captured (and discarded) Execute exceptions
2026-08-07 21:39:30 -04:00
Cody Robibero 830d93e446 Merge pull request #17554 from IDisposable/fix/faster-update-items
Speed up UpdateOrInsertItems for related information
2026-08-07 21:39:10 -04:00
Cody Robibero d696cea9bc Merge pull request #17558 from IDisposable/fix/set-cache-to-private
Switch SQLite connection Cache to Private
2026-08-07 21:38:59 -04:00
queeup e83125c17c Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2026-08-08 01:38:51 +00:00
Vitalijus c5da7db120 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-08 01:38:51 +00:00
Cody Robibero f9b7f2edf7 Merge pull request #17536 from gnattu/fix-concurrent-racing
Fix concurrent ffmpeg segment racing
2026-08-07 21:38:45 -04:00
Vitalijus 4192a4dd32 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-07 19:43:35 +00:00
Vitalijus 84ffe668d0 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-07 19:25:33 +00:00
brandon d6da6906a4 Batch people lookups when building item DTOs
GetBaseItemDtos already batch fetches user data, child counts, played counts
and artists before its per item loop, but AttachPeople still ran one GetPeople
query per item. Rendering a page of items (for example a large playlist) fired
one extra query per row.

Add GetPeopleByItems to IPeopleRepository, which reads every requested item in a
single query over the people mapping table and returns full PersonInfo (role,
type and sort order) grouped by item id. GetBaseItemDtos prefetches this once
when the People field is requested and passes it into AttachPeople, which reads
from the batch instead of querying per item. The single item GetBaseItemDto path
keeps its existing per item behaviour when no batch is supplied.

Adds a DtoService test asserting people resolve from the batch and the per item
GetPeople is never called.
2026-08-07 11:59:17 -04:00
Shadowghost b7e2d21038 Fix EF core designer drifts 2026-08-07 17:51:14 +02:00
vavallee e120b7f2dd Stop image endpoints from upscaling beyond the source resolution
ImageHelper.GetNewImageSize passed the caller-supplied width/height straight
through to SkiaEncoder.EncodeImage, which allocates an SKImageInfo of exactly
that size. Nothing bounded those values against the source image, so a request
like Items/<id>/Images/Primary?width=23100&height=23100 made the server allocate
and resample a 23100x23100 surface from, say, a 600x336 poster: the reporter
measured 100% of a core for 10-15 minutes and 6-12 GB resident per request.
The item images endpoints do not require authentication, so any caller who knows
an item id can trigger this, and varying the size by one pixel misses the cache
every time.

Add DrawingUtils.ScaleDownToFit, which scales a size down uniformly until it
fits inside a bounding box and returns it unchanged if it already does, and
apply it in GetNewImageSize against the original image dimensions. Requests
that ask for more pixels than the source now get the source resolution back,
scaled to the requested aspect ratio. Downscaling paths are untouched, and
DrawingUtils.Resize keeps its existing behaviour for the transcoding callers in
EncodingJobInfo and StreamInfo, which legitimately size video output.
ResizeFill already refused to upscale; this makes width/height consistent
with fillWidth/fillHeight.

Fixes #17056.
2026-08-07 12:33:37 -03:00
Vitalijus 6c37a6ef8b Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-07 11:11:10 +00:00
Shadowghost 9162c17834 Apply review suggestions 2026-08-07 07:23:50 +02:00
Vitalijus 929834c982 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-06 19:11:07 +00:00
Shadowghost 5b3da3bcd7 Cleanup and simplify query helpers 2026-08-06 12:42:53 +02:00
Keleti Márton 4860f945a7 Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/
2026-08-06 09:43:17 +00:00
simonanter 9f74fd7354 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/
2026-08-06 09:43:16 +00:00
Marc Brooks 1c5c95ad1d Move the deletion of old related info to just before the save
This makes the deletion of BaseItemProviders, BaseItemImageInfos, and BaseItemMetadataFields happen in batch as  a contiguous block so the lock isn't held across items, just before the bulk SaveChanges.
2026-08-06 01:00:17 -05:00
Marc Brooks dd81e52f81 Add warning that PessimisticLockBehavior is unsafe
Since it uses ReaderWriterLockSlim, it gets tripped up if the transaction
is continued across an await boundary on a different thread.
2026-08-06 00:52:34 -05:00
Marc Brooks 868ad089f8 Fix captured (and discarded) Execute exceptions
Using ExecuteAndCapture (and the async version) requires something to manage the captured exception, which we don't do. Errors would be silently dropped and the writes/deletes treated as if they succeeded.
2026-08-06 00:44:51 -05:00
Marc Brooks 2a08a30982 Switch SQLite connection Cache to Private
If left at Default,  sqlite3_enable_shared_cache is process-global, so a plugin enabling it makes these connections share a cache too.
Contention then surfaces as SQLITE_LOCKED ("database table is locked"), which the busy handler does not cover,  busy_timeout is skipped and the command fails at CommandTimeout instead.
2026-08-06 00:36:01 -05:00
Marc Brooks 812c819162 Speed up search for existing item values
Replace the O(n²)  array .First scan with a dictionary lookup
2026-08-05 22:48:26 -05:00
Marc Brooks 234a49903b Use a HashSet for existing items
Makes the test O(log n)
2026-08-05 22:48:13 -05:00
Cody Robibero cd5962df50 Merge pull request #17315 from Florin-Popescu/replaygain-missing-tags
Parse ReplayGain album gain field
2026-08-05 19:04:05 -04:00
krvi 9d99164c19 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-05 22:41:37 +00:00
Kityn 2db10787af Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pl/
2026-08-05 22:41:34 +00:00
Luis Fernando Illapa 614e5c7912 Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/
2026-08-05 22:41:27 +00:00
Cody Robibero 17b0453cfb Merge pull request #17466 from Shadowghost/fix-byname-queries
Improve People deduplication, fix search and restrict ItemByName responses
2026-08-05 18:39:55 -04:00
Cody Robibero d5a5b56484 Merge pull request #17537 from vdatanet/fix/pcm-wav-transcode
Fix PCM audio transcoding to wav returning HTTP 500 and headerless output
2026-08-05 18:39:42 -04:00
Cody Robibero ef1ce3d6eb Merge pull request #17549 from theguymadmax/revert-livetv-channel-icon-refresh
Revert "Refresh Live TV channel icons on every guide update."
2026-08-05 18:38:51 -04:00
Cody Robibero 07fb350333 Merge pull request #17524 from Shadowghost/fix-extra-discovery
Keep folder extras with the item that owns the folder
2026-08-05 18:38:31 -04:00
theguymadmax f49a501f71 Revert "Refresh Live TV channel icons on every guide update."
This reverts commit 372c1681d8.
2026-08-05 14:02:28 -04:00
Bond-009 f6ca06e9bf Merge pull request #17507 from jellyfin/renovate/sharpcompress-0.x
Update dependency SharpCompress to 0.50.4
2026-08-05 18:07:52 +02:00
renovate[bot] ef84408199 Update dependency SharpCompress to 0.50.4 2026-08-05 15:55:11 +00:00
Luis Fernando Illapa 0db3759325 Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/
2026-08-05 15:54:13 +00:00
rimasx df1611cf95 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/
2026-08-05 15:54:13 +00:00
Dan Johansen a6d09815f2 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/
2026-08-05 15:54:13 +00:00
Helak 38aa870514 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/
2026-08-05 15:54:13 +00:00
Bond-009 d13c65229d Merge pull request #17493 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.37.6
2026-08-05 17:54:07 +02:00
Shadowghost 61e75599b3 Project the lowered person credit values once when updating people 2026-08-05 13:21:57 +02:00
Shadowghost b2485af6e9 Merge remote-tracking branch 'upstream/master' into fix-byname-queries
# Conflicts:
#	Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
2026-08-05 12:15:30 +02:00
vdatanet 4adaf7f146 Fix by-name endpoints reporting TotalRecordCount=0 next to a populated Items array
`GetItemValues` -- the shared path behind `/Artists`, `/AlbumArtists`, `/Genres`,
`/MusicGenres` and `/Studios` -- disabled the total record count whenever the
query carried no `Limit`:

    if (!filter.Limit.HasValue)
    {
        filter.EnableTotalRecordCount = false;
    }

A request without an explicit limit therefore came back with N entries in `Items`
and `TotalRecordCount = 0`. Clients that page on the reported total -- the
documented contract every other list endpoint honours -- read that as an empty
library. `/Items` and `/Persons` do not share this path and report the count
correctly, which is what makes the inconsistency visible from the outside.

Measured against master with a 62-track music library:

    GET /Artists?UserId=...              -> TotalRecordCount=0  Items=5
    GET /Artists?UserId=...&limit=100    -> TotalRecordCount=5  Items=5

Dropping the block costs nothing: `representativeIds` is materialised into a
`List<Guid>` a few lines below regardless, so `.Count` was already available and
the count is now reported from it. Callers that genuinely want to skip the count
still can -- `EnableTotalRecordCount = false` is honoured as before.

The block also mutated the caller's own query object, so a query instance reused
across calls silently lost its total after the first limitless one. That is
covered by a test as well.
2026-08-05 11:34:33 +02:00
krvi 563a00faaa Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-05 01:46:45 +00:00
krvi 5e33a2d376 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-04 22:40:18 +00:00
Cody Robibero d9d9efc96e Merge pull request #17523 from Shadowghost/fix-resume-container-folders
Only treat series and seasons as resumable folders
2026-08-04 18:01:13 -04:00
Vitalijus dd1380e562 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-04 21:58:15 +00:00
Translation expert cfb00b58a2 Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/
2026-08-04 21:58:15 +00:00
Cody Robibero 990ab18353 Merge pull request #17528 from Shadowghost/websocket-logs-debug
Degrade ForceKeepAlive logs to debug
2026-08-04 17:58:09 -04:00
vdatanet 3578e9a332 Fix PCM audio transcoding to wav returning HTTP 500 and headerless output
`GetProgressiveAudioFullCommandLine` forced the raw PCM muxer and a bogus
sample rate whenever the audio encoder was `pcm_*`, regardless of the
container the client asked for. Two separate failures came out of it:

- `-ar ` + `state.BaseRequest.AudioBitRate` used a *bitrate* as a *sample
  rate*, and `AudioBitRate` is optional. When it is absent the argument
  degrades to a bare `-ar`, ffmpeg aborts with `Expected number for ar but
  found: -ar` / `Error opening output files: Invalid argument` (exit 234)
  and the request fails with HTTP 500. Every `GET /Audio/{id}/stream.wav`
  that does not carry an explicit `AudioBitRate` hits this.
  The sample rate was already being set correctly a few lines below from
  `OutputAudioSampleRate`, so the line is dropped rather than repaired.

- `-f s16le` overrode the muxer even for a real container. A request that
  did supply a bitrate (`/Audio/{id}/universal` passes
  `MaxStreamingBitrate`) survived the first bug but produced raw headerless
  samples served under an `audio/wav` content type, so clients saw a body
  with no RIFF header. The raw muxer is now only forced when the requested
  container is actually raw PCM, which keeps the I2S/MCU route from #10321
  working.

Also drop the `containerInternal = ".pcm"` assignment in
`StreamingHelpers.GetStreamingState`: it is written after
`state.OutputContainer` has already been read from the same variable and is
never read again, so it has no effect and only obscures where the output
container comes from.

Verified against ffmpeg 8.1.2 with a 96 kHz FLAC source: before, the wav
command line exits 234; after, it produces a valid `RIFF/WAVE` 48 kHz stereo
`pcm_s16le` file, while the raw `.pcm` route still yields exactly
2 s x 48000 x 2ch x 2 B = 384000 bytes of headerless samples.
2026-08-04 19:52:29 +02:00
renovate[bot] f60cfc788b Update github/codeql-action action to v4.37.6 2026-08-04 16:41:10 +00:00
gnattu e2586eed9b Fix concurrent ffmpeg segment racing
This is a nasty one. The failure mode is:

1. Request A started FFmpeg and waited for a segment.
2. Request B requested an earlier or far away segment.
3. Jellyfin thought FFmpeg should to restart at a different position.
4. Request B killed the existing transcoding job.
5. Killing that job cancelled the same token request A was using.
6. The cancellation produced http 500 to request A.

To fix this:

we lock transcoding job state changes and segment handling per playlist, and use a thread safe counter to track how many http responses are still using each job’s segments. A job is only stopped or replaced once that counter reaches zero.
2026-08-05 00:33:42 +08:00
Shed Shedson 7fbc1ff8c0 Translated using Weblate (Icelandic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/is/
2026-08-04 11:29:56 +00:00
Yunseo Jung 5767a3a800 Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ko/
2026-08-04 11:29:55 +00:00
Vincenzo Reale 4b51ef2c2f Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-08-04 11:29:55 +00:00
krvi f50fc0fa86 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-03 23:01:06 +00:00
potuzhnyj a817391f67 Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/
2026-08-03 23:01:06 +00:00
Erik W cc877229b1 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sv/
2026-08-03 23:01:05 +00:00
Vitalijus a3e7024355 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-03 23:01:04 +00:00
Shadowghost d3c7aa6bd2 Fix /Persons favourites timing out by removing the correlated favourites join 2026-08-04 00:40:53 +02:00
Shadowghost 40601523e7 Degrade ForceKeepAlive logs to debug 2026-08-03 21:30:22 +02:00
krvi e3664a9561 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-03 18:59:02 +00:00
nextlooper42 a1ed4b9001 Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/
2026-08-03 18:59:02 +00:00
Vincenzo Reale e09492de62 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-08-03 18:59:01 +00:00
myrad2267 d10381a0e2 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/
2026-08-03 18:59:01 +00:00
myrad2267 9596115c45 Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr_CA/
2026-08-03 18:59:00 +00:00
Translation expert ed0be243e9 Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/
2026-08-03 18:58:59 +00:00
Shadowghost f502fd312e Fix test 2026-08-03 15:29:32 +02:00
rlauuzo 9d0dffe818 Clear metadata provider cache when provider parts are registered
AddParts replaces _metadataProviders but left _metadataProviderCache
holding provider arrays from the previous registration. Invalidate the
cache so repeated AddParts calls cannot serve stale providers, and give
the previously uncalled ClearMetadataProviderCache its intended caller.
2026-08-03 11:56:08 +02:00
Shadowghost 4e2089b6a1 Keep folder extras with the item that owns the folder 2026-08-03 10:50:33 +02:00
Shadowghost 55518c06ee Only treat series and seasons as resumable folders 2026-08-03 09:01:55 +02:00
Shadowghost a272efb9a2 Fix disabled plugins being re-enabled on restart 2026-08-03 08:48:09 +02:00
Robin 33a8cdfc0b Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_419/
2026-08-03 06:20:33 +00:00
Robin 38c6c5b478 Translated using Weblate (Spanish (Mexico))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_MX/
2026-08-03 06:20:32 +00:00
Robin d759df03b5 Translated using Weblate (Spanish (Argentina))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/
2026-08-03 06:20:31 +00:00
Kaz e2c1121079 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/
2026-08-03 06:20:29 +00:00
無情天 fe3585577b Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/
2026-08-03 01:53:49 +00:00
Shadowghost 8044534774 Merge remote-tracking branch 'upstream/master' into fix-byname-queries 2026-08-02 22:12:57 +02:00
Shadowghost 705368ee49 Merge remote-tracking branch 'upstream/master' into fix-byname-queries
# Conflicts:
#	src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
2026-08-01 21:41:16 +02:00
Shadowghost cbc2c7c323 Preserve multiple roles per person type instead of deduping credits by name and type 2026-08-01 20:35:27 +02:00
GOvEy1nw 0915a61c19 fix(images): narrow cache path test seam 2026-07-30 09:20:01 +01:00
Shadowghost e123a13e38 Apply the by-name access exemption in the search candidate query 2026-07-30 09:53:44 +02:00
GOvEy1nw 3da05330b2 fix(images): disambiguate progress overlay cache keys 2026-07-29 17:37:36 +01:00
Shadowghost a94588497c Fix Folder access filtering 2026-07-28 23:13:10 +02:00
Shadowghost 9a258c089d Restrict people, genres, studios and artists to names backed by an item the user can access 2026-07-28 21:28:56 +02:00
Shadowghost 5f5c71ab75 Make the /Persons de-duplication use an index instead of grouping the table 2026-07-28 20:18:02 +02:00
Florin Popescu f6f0fed2d1 skip album ReplayGain parsing if no album or value already present 2026-07-28 19:53:50 +02:00
Florin Popescu e2adf2e2bf added null checks 2026-07-28 19:53:50 +02:00
Florin Popescu 78b535b229 parse replaygain reference loudness & album gain fields 2026-07-28 19:53:50 +02:00
338 changed files with 23922 additions and 2406 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.10",
"version": "10.0.11",
"commands": [
"dotnet-ef"
]
+3 -3
View File
@@ -32,13 +32,13 @@ jobs:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
+6 -1
View File
@@ -143,6 +143,7 @@
- [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
- [st7105](https://github.com/st7105)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
- [ssenart](https://github.com/ssenart)
@@ -236,6 +237,10 @@
- [Lampan-git](https://github.com/Lampan-git)
- [elio42](https://github.com/elio42)
- [rwebster85](https://github.com/rwebster85)
- [Florin-Popescu](https://github.com/Florin-Popescu)
- [m0g3r](https://github.com/m0g3r)
- [martin-77](https://github.com/martin-77)
- [Oggeb1](https://github.com/Oggeb1)
# Emby Contributors
@@ -246,7 +251,7 @@
- [Mark2xv](https://github.com/Mark2xv)
- [ScottRapsey](https://github.com/ScottRapsey)
- [skynet600](https://github.com/skynet600)
- [Cheesegeezer](https://githum.com/Cheesegeezer)
- [Cheesegeezer](https://github.com/Cheesegeezer)
- [Radeon](https://github.com/radeonorama)
- [gcw07](https://github.com/gcw07)
- [SivaramAdhiappan](https://github.com/shivaram1190)
+26 -26
View File
@@ -9,7 +9,7 @@
<PackageVersion Include="AutoFixture.Xunit3" Version="4.19.0" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.6.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.6.1" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
@@ -26,28 +26,28 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.10" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.11" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -68,7 +68,7 @@
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpCompress" Version="0.50.1" />
<PackageVersion Include="SharpCompress" Version="0.50.4" />
<PackageVersion Include="SharpFuzz" Version="2.3.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.4" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.4" />
@@ -78,11 +78,11 @@
<PackageVersion Include="Svg.Skia" Version="3.7.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.2.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.3" />
<PackageVersion Include="System.Text.Json" Version="10.0.10" />
<PackageVersion Include="System.Text.Json" Version="10.0.11" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.15.3" />
<PackageVersion Include="z440.atl.core" Version="7.16.0" />
<PackageVersion Include="TMDbLib" Version="3.0.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="UTF.Unknown" Version="2.7.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="Xunit.v3.Priority" Version="1.1.18" />
+1 -1
View File
@@ -151,7 +151,7 @@ namespace Emby.Naming.Common
CleanStrings =
[
@"^\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|web-dl|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>.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@@ -44,7 +44,14 @@ namespace Emby.Naming.ExternalFiles
}
var extension = Path.GetExtension(path.AsSpan());
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
// .idx carries VobSub per-track language metadata. Recognize it here rather
// than adding it to NamingOptions.SubtitleFileExtensions, which also gates
// subtitle uploads/saves.
var isVobSubIndex = _type == DlnaProfileType.Subtitle && extension.Equals(".idx", StringComparison.OrdinalIgnoreCase);
if (!isVobSubIndex
&& !(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Lyric && _namingOptions.LyricFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
{
+4 -2
View File
@@ -158,7 +158,9 @@ namespace Emby.Naming.TV
if (nextIndex >= name.Length
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
{
if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
// A range cannot end before it starts, so a lower number belongs to the episode title rather than to a range.
if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)
&& num >= result.EpisodeNumber)
{
result.EndingEpisodeNumber = num;
}
@@ -226,7 +228,7 @@ namespace Emby.Naming.TV
info.SeriesName = result.SeriesName;
}
if (!info.EndingEpisodeNumber.HasValue && info.EpisodeNumber.HasValue)
if (!info.EndingEpisodeNumber.HasValue && result.EndingEpisodeNumber >= info.EpisodeNumber)
{
info.EndingEpisodeNumber = result.EndingEpisodeNumber;
}
@@ -107,7 +107,8 @@ namespace Emby.Server.Implementations.Collections
SaveLocalMetadata = true
};
var name = _localizationManager.GetLocalizedString("Collections");
// This names a library for the whole server, so ignore the requesting client's language.
var name = _localizationManager.GetServerLocalizedString("Collections");
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
+109 -21
View File
@@ -185,6 +185,13 @@ namespace Emby.Server.Implementations.Dto
allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
}
// Batch-fetch by-name item counts to avoid N+1 queries
Dictionary<Guid, ItemCounts>? itemCountsBatch = null;
if (options.ContainsField(ItemFields.ItemCounts))
{
itemCountsBatch = GetItemCountsBatch(accessibleItems, user);
}
// Batch-fetch child counts for all folders to avoid N+1 queries
Dictionary<Guid, int>? childCountBatch = null;
if (options.ContainsField(ItemFields.ChildCount))
@@ -192,7 +199,7 @@ namespace Emby.Server.Implementations.Dto
var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
if (folderIds.Count > 0)
{
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user);
}
}
@@ -242,6 +249,29 @@ namespace Emby.Server.Implementations.Dto
artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
}
// Batch-fetch people across all items to avoid one GetPeople query per item.
IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null;
if (options.ContainsField(ItemFields.People))
{
var peopleItemIds = accessibleItems.Where(i => i.SupportsPeople).Select(i => i.Id).ToList();
if (peopleItemIds.Count > 0)
{
peopleBatch = _libraryManager.GetPeopleByItems(peopleItemIds);
}
}
// Batch-detect which videos own alternate versions to avoid the per-item alternate-version
// queries in MediaSourceCount. Videos absent from this set have a single media source.
IReadOnlySet<Guid>? alternateVersionItemIds = null;
if (options.ContainsField(ItemFields.MediaSourceCount))
{
var versionItemIds = accessibleItems.OfType<Video>().Select(i => i.Id).ToList();
if (versionItemIds.Count > 0)
{
alternateVersionItemIds = _libraryManager.GetItemIdsWithAlternateVersions(versionItemIds);
}
}
for (int index = 0; index < accessibleItems.Count; index++)
{
var item = accessibleItems[index];
@@ -255,7 +285,9 @@ namespace Emby.Server.Implementations.Dto
childCountBatch,
playedCountBatch,
artistsBatch,
resumeDataBatch?.GetValueOrDefault(item.Id));
resumeDataBatch?.GetValueOrDefault(item.Id),
peopleBatch,
alternateVersionItemIds);
if (item is LiveTvChannel tvChannel)
{
@@ -268,7 +300,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ItemCounts))
{
SetItemByNameInfo(dto, user);
SetItemByNameInfo(dto, user, itemCountsBatch);
}
returnItems[index] = dto;
@@ -317,7 +349,9 @@ namespace Emby.Server.Implementations.Dto
Dictionary<Guid, int>? childCountBatch = null,
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null,
VersionResumeData? resumeData = null)
VersionResumeData? resumeData = null,
IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null,
IReadOnlySet<Guid>? alternateVersionItemIds = null)
{
var dto = new BaseItemDto
{
@@ -331,7 +365,15 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.People))
{
AttachPeople(dto, item, user);
IReadOnlyList<PersonInfo>? prefetchedPeople = null;
if (peopleBatch is not null)
{
// The batch omits items with no people, so a miss means "no people",
// not "not fetched". Use an empty list to skip the per-item query.
prefetchedPeople = peopleBatch.GetValueOrDefault(item.Id) ?? [];
}
AttachPeople(dto, item, user, prefetchedPeople);
}
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
@@ -378,7 +420,7 @@ namespace Emby.Server.Implementations.Dto
AttachStudios(dto, item);
}
AttachBasicFields(dto, item, owner, options, artistsBatch, user);
AttachBasicFields(dto, item, owner, options, artistsBatch, user, alternateVersionItemIds);
if (options.ContainsField(ItemFields.CanDelete))
{
@@ -483,14 +525,36 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
private void SetItemByNameInfo(BaseItemDto dto, User? user)
private Dictionary<Guid, ItemCounts> GetItemCountsBatch(IReadOnlyList<BaseItem> items, User? user)
{
var result = new Dictionary<Guid, ItemCounts>();
foreach (var group in items.GroupBy(item => item.GetBaseItemKind()))
{
if (!_relatedItemKinds.TryGetValue(group.Key, out var relatedItemKinds))
{
continue;
}
var ids = group.Select(item => item.Id).ToArray();
foreach (var (id, counts) in _libraryManager.GetItemCountsForNameItems(group.Key, ids, relatedItemKinds, user))
{
result[id] = counts;
}
}
return result;
}
private void SetItemByNameInfo(BaseItemDto dto, User? user, IReadOnlyDictionary<Guid, ItemCounts>? prefetchedCounts = null)
{
if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
{
return;
}
var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
var counts = prefetchedCounts?.GetValueOrDefault(dto.Id)
?? _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
dto.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount;
@@ -576,7 +640,11 @@ namespace Emby.Server.Implementations.Dto
// For these types we can try to optimize and assume these values will be equal
if (item is MusicAlbum || item is Season || item is Playlist)
{
dto.ChildCount = dto.RecursiveItemCount;
if (dto.RecursiveItemCount > 0)
{
dto.ChildCount = dto.RecursiveItemCount;
}
var folderChildCount = folder.LinkedChildren.Length;
// The default is an empty array, so we can't reliably use the count when it's empty
if (folderChildCount > 0)
@@ -661,7 +729,8 @@ namespace Emby.Server.Implementations.Dto
return count;
}
// Fall back to individual query for special cases (Series, Season, etc.)
// Only reached when no batch was computed: the batch holds an entry for every folder it
// was asked about, zero included.
return folder.GetChildCount(user);
}
@@ -742,12 +811,18 @@ namespace Emby.Server.Implementations.Dto
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <param name="user">The requesting user.</param>
private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null)
/// <param name="prefetchedPeople">People fetched in batch by the caller; when null the people are queried per item.</param>
private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null, IReadOnlyList<PersonInfo>? prefetchedPeople = null)
{
// When rendering a page of items the caller batch-fetches people for every item up
// front and passes them in, avoiding one GetPeople query per item. Fall back to the
// per-item query for the single item path where no batch is available.
var source = prefetchedPeople ?? _libraryManager.GetPeople(item);
// Ordering by person type to ensure actors and artists are at the front.
// This is taking advantage of the fact that they both begin with A
// This should be improved in the future
var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
var people = source.OrderBy(i => i.SortOrder ?? int.MaxValue)
.ThenBy(i =>
{
if (i.IsType(PersonKind.Actor))
@@ -957,7 +1032,8 @@ namespace Emby.Server.Implementations.Dto
/// <param name="options">The options.</param>
/// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
/// <param name="user">The user, for per-user values such as the accessible media source count.</param>
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null, User? user = null)
/// <param name="alternateVersionItemIds">Optional pre-fetched set of item IDs that own alternate versions, shared across a batch of items.</param>
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null, User? user = null, IReadOnlySet<Guid>? alternateVersionItemIds = null)
{
if (options.ContainsField(ItemFields.DateCreated))
{
@@ -1271,15 +1347,27 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.MediaSourceCount))
{
// Match the per-user filtering of the media sources: versions the user cannot
// access are not selectable, so they must not count towards the badge either.
var mediaSourceCount = user is null
|| (!video.PrimaryVersionId.HasValue && video.LinkedAlternateVersions.Length == 0 && !video.HasLocalAlternateVersions)
? video.MediaSourceCount
: video.GetAllVersions().Count(v => v.Id.Equals(video.Id) || v.IsVisibleStandalone(user));
if (mediaSourceCount != 1)
// A video with no primary version and no alternate versions always has a single
// media source. Only compute the count for videos that might have more: a primary
// version, or membership in the batch's set of items that own alternate versions.
// Without the batch we can't rule it out, so fall back to computing (the single-item
// path). Everything else is the common case and keeps the default count of one.
var mayHaveAlternateVersions = alternateVersionItemIds is null
|| video.PrimaryVersionId.HasValue
|| alternateVersionItemIds.Contains(video.Id);
if (mayHaveAlternateVersions)
{
dto.MediaSourceCount = mediaSourceCount;
// Match the per-user filtering of the media sources: versions the user cannot
// access are not selectable, so they must not count towards the badge either.
var mediaSourceCount = user is null
|| (!video.PrimaryVersionId.HasValue && video.LinkedAlternateVersions.Length == 0 && !video.HasLocalAlternateVersions)
? video.MediaSourceCount
: video.GetAllVersions().Count(v => v.Id.Equals(video.Id) || v.IsVisibleStandalone(user));
if (mediaSourceCount != 1)
{
dto.MediaSourceCount = mediaSourceCount;
}
}
}
@@ -27,6 +27,11 @@ namespace Emby.Server.Implementations.EntryPoints;
/// </summary>
public sealed class LibraryChangedNotifier : IHostedService, IDisposable
{
// A batch holds a live reference to every item it names, so it has to stay small enough that a
// library scan - which changes items faster than any batch window closes - cannot grow it without
// bound. Reached only by a scan; interactive use closes a batch on the window long before this.
internal const int MaxBatchSize = 2000;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IProviderManager _providerManager;
@@ -35,11 +40,11 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
private readonly ILogger<LibraryChangedNotifier> _logger;
private readonly Lock _libraryChangedSyncLock = new();
private readonly List<Folder> _foldersAddedTo = new();
private readonly List<Folder> _foldersRemovedFrom = new();
private readonly List<BaseItem> _itemsAdded = new();
private readonly List<BaseItem> _itemsRemoved = new();
private readonly List<BaseItem> _itemsUpdated = new();
private readonly Dictionary<Guid, Folder> _foldersAddedTo = [];
private readonly Dictionary<Guid, Folder> _foldersRemovedFrom = [];
private readonly Dictionary<Guid, BaseItem> _itemsAdded = [];
private readonly Dictionary<Guid, BaseItem> _itemsRemoved = [];
private readonly Dictionary<Guid, BaseItem> _itemsUpdated = [];
private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new();
private Timer? _libraryUpdateTimer;
@@ -173,7 +178,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
private void OnLibraryItemRemoved(object? sender, ItemChangeEventArgs e)
=> OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom);
private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder>? foldersList)
private void OnLibraryChange(BaseItem item, BaseItem parent, Dictionary<Guid, BaseItem> itemsList, Dictionary<Guid, Folder>? foldersList)
{
if (!FilterItem(item))
{
@@ -182,23 +187,28 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
lock (_libraryChangedSyncLock)
{
var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration);
// The window runs from the first change of a batch and is never extended. Extending it on
// every change would keep a library scan's batch open for the whole scan, and the batch
// holds the items it names alive, so it would grow to the size of the library.
if (_libraryUpdateTimer is null)
{
var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration);
_libraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan);
}
else
{
_libraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan);
}
if (foldersList is not null && parent is Folder folder)
{
foldersList.Add(folder);
foldersList[folder.Id] = folder;
}
itemsList.Add(item);
itemsList[item.Id] = item;
// A window long enough to cover a burst still has to give way once the batch is large
// enough to be worth sending on its own.
if (_itemsAdded.Count + _itemsRemoved.Count + _itemsUpdated.Count >= MaxBatchSize)
{
_libraryUpdateTimer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
}
}
}
@@ -211,22 +221,16 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
List<BaseItem> itemsRemoved;
lock (_libraryChangedSyncLock)
{
// Remove dupes in case some were saved multiple times
foldersAddedTo = _foldersAddedTo
.DistinctBy(x => x.Id)
.ToList();
foldersRemovedFrom = _foldersRemovedFrom
.DistinctBy(x => x.Id)
.ToList();
foldersAddedTo = _foldersAddedTo.Values.ToList();
foldersRemovedFrom = _foldersRemovedFrom.Values.ToList();
itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i))
.DistinctBy(x => x.Id)
.Where(e => !_itemsAdded.ContainsKey(e.Key))
.Select(e => e.Value)
.ToList();
itemsAdded = _itemsAdded.ToList();
itemsRemoved = _itemsRemoved.ToList();
itemsAdded = _itemsAdded.Values.ToList();
itemsRemoved = _itemsRemoved.Values.ToList();
if (_libraryUpdateTimer is not null)
{
@@ -241,6 +245,15 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
_foldersRemovedFrom.Clear();
}
if (itemsAdded.Count == 0
&& itemsUpdated.Count == 0
&& itemsRemoved.Count == 0
&& foldersAddedTo.Count == 0
&& foldersRemovedFrom.Count == 0)
{
return;
}
await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
}
@@ -18,15 +18,17 @@ namespace Emby.Server.Implementations.EntryPoints
public sealed class UserDataChangeNotifier : IHostedService, IDisposable
{
private const int UpdateDuration = 500;
internal const int MaxBatchSize = 2000;
private readonly ISessionManager _sessionManager;
private readonly IUserDataManager _userDataManager;
private readonly IUserManager _userManager;
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
private readonly Dictionary<Guid, Dictionary<Guid, BaseItem>> _changedItems = [];
private readonly Lock _syncLock = new();
private Timer? _updateTimer;
private int _changedItemCount;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataChangeNotifier"/> class.
@@ -69,50 +71,64 @@ namespace Emby.Server.Implementations.EntryPoints
lock (_syncLock)
{
if (_updateTimer is null)
{
_updateTimer = new Timer(
UpdateTimerCallback,
null,
UpdateDuration,
Timeout.Infinite);
}
else
{
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
}
// The window runs from the first change of a batch and is never extended, so a stream
// of changes that never pauses - a library scan - still closes its batches instead of
// holding every item it touched alive until the stream stops.
_updateTimer ??= new Timer(
UpdateTimerCallback,
null,
UpdateDuration,
Timeout.Infinite);
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys))
if (!_changedItems.TryGetValue(e.UserId, out Dictionary<Guid, BaseItem>? keys))
{
keys = new List<BaseItem>();
keys = [];
_changedItems[e.UserId] = keys;
}
keys.Add(e.Item);
var baseItem = e.Item;
// Go up one level for indicators
if (baseItem is not null)
{
Track(keys, baseItem);
var parent = baseItem.GetOwner() ?? baseItem.GetParent();
if (parent is not null)
{
keys.Add(parent);
Track(keys, parent);
}
}
// A window long enough to cover a burst still has to give way once the batch is
// large enough to be worth sending on its own.
if (_changedItemCount >= MaxBatchSize)
{
_updateTimer.Change(0, Timeout.Infinite);
}
}
}
private void Track(Dictionary<Guid, BaseItem> keys, BaseItem item)
{
var before = keys.Count;
keys[item.Id] = item;
if (keys.Count != before)
{
_changedItemCount++;
}
}
private async void UpdateTimerCallback(object? state)
{
List<KeyValuePair<Guid, List<BaseItem>>> changes;
List<KeyValuePair<Guid, Dictionary<Guid, BaseItem>>> changes;
lock (_syncLock)
{
// Remove dupes in case some were saved multiple times
changes = _changedItems.ToList();
_changedItems.Clear();
_changedItemCount = 0;
if (_updateTimer is not null)
{
@@ -121,17 +137,22 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
if (changes.Count == 0)
{
return;
}
foreach (var (userId, changedItems) in changes)
{
await _sessionManager.SendMessageToUserSessions(
[userId],
SessionMessageType.UserDataChanged,
() => GetUserDataChangeInfo(userId, changedItems),
() => GetUserDataChangeInfo(userId, changedItems.Values),
default).ConfigureAwait(false);
}
}
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, IEnumerable<BaseItem> changedItems)
{
var user = _userManager.GetUserById(userId)
?? throw new ArgumentException("Invalid user ID", nameof(userId));
@@ -140,7 +161,6 @@ namespace Emby.Server.Implementations.EntryPoints
{
UserId = userId,
UserDataList = changedItems
.DistinctBy(x => x.Id)
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
@@ -8,6 +8,7 @@ using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -21,6 +22,7 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly IDirectoryService _directoryService;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
/// <summary>
@@ -47,6 +49,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="libraryManager">The library manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="directoryService">The directory service.</param>
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
public LibraryMonitor(
@@ -54,6 +57,7 @@ namespace Emby.Server.Implementations.IO
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
IDirectoryService directoryService,
IHostApplicationLifetime appLifetime,
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
{
@@ -61,6 +65,7 @@ namespace Emby.Server.Implementations.IO
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
_directoryService = directoryService;
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
appLifetime.ApplicationStarted.Register(Start);
@@ -363,6 +368,8 @@ namespace Emby.Server.Implementations.IO
return;
}
_directoryService.Invalidate(path);
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
foreach (var i in _tempIgnoredPaths.Keys)
{
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -15,7 +16,6 @@ using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks.Tasks;
using Emby.Server.Implementations.Sorting;
@@ -35,7 +35,6 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
@@ -75,7 +74,6 @@ namespace Emby.Server.Implementations.Library
private readonly Lazy<IProviderManager> _providerManagerFactory;
private readonly Lazy<IUserViewManager> _userViewManagerFactory;
private readonly IServerApplicationHost _appHost;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
private readonly IItemPersistenceService _persistenceService;
@@ -88,6 +86,7 @@ namespace Emby.Server.Implementations.Library
private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager;
private readonly ILocalizationManager _localization;
private readonly IDirectoryService _directoryService;
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
private readonly IMediaStreamRepository _mediaStreamRepository;
@@ -122,7 +121,6 @@ namespace Emby.Server.Implementations.Library
/// <param name="fileSystem">The file system.</param>
/// <param name="providerManagerFactory">The provider manager.</param>
/// <param name="userViewManagerFactory">The user view manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="persistenceService">The item persistence service.</param>
/// <param name="nextUpService">The next up service.</param>
@@ -148,7 +146,6 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem,
Lazy<IProviderManager> providerManagerFactory,
Lazy<IUserViewManager> userViewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IItemPersistenceService persistenceService,
INextUpService nextUpService,
@@ -174,7 +171,6 @@ namespace Emby.Server.Implementations.Library
_fileSystem = fileSystem;
_providerManagerFactory = providerManagerFactory;
_userViewManagerFactory = userViewManagerFactory;
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_persistenceService = persistenceService;
_nextUpService = nextUpService;
@@ -189,6 +185,7 @@ namespace Emby.Server.Implementations.Library
_pathManager = pathManager;
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
_localization = localization;
_directoryService = directoryService;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -412,6 +409,13 @@ namespace Emby.Server.Implementations.Library
}
_persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
// Evict the deleted items from the cache and announce each removal.
foreach (var (item, _, _) in pathMaps)
{
_cache.TryRemove(item.Id, out _);
ReportItemRemoved(item, item.GetOwner() ?? item.GetParent());
}
}
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
@@ -611,6 +615,12 @@ namespace Emby.Server.Implementations.Library
folder.UserData = null;
}
// Announce the descendants before the item itself.
foreach (var child in children)
{
ReportItemRemoved(child, item);
}
ReportItemRemoved(item, parent);
}
@@ -1196,6 +1206,12 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault();
}
/// <inheritdoc />
public Guid GetPersonId(string name)
{
return GetItemByNameId<Person>(Person.GetPath(name));
}
/// <inheritdoc />
public Person? GetPerson(string name)
{
@@ -1209,6 +1225,33 @@ namespace Emby.Server.Implementations.Library
return null;
}
/// <inheritdoc />
public Person GetOrCreatePerson(string name)
{
var existing = GetPerson(name);
if (existing is not null)
{
return existing;
}
var path = Person.GetPath(name);
var info = Directory.CreateDirectory(path);
var item = new Person
{
Name = name,
Id = GetItemByNameId<Person>(path),
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
Path = path
};
item.PresentationUniqueKey = item.CreatePresentationUniqueKey();
CreateItem(item, null);
return item;
}
/// <summary>
/// Gets the studio.
/// </summary>
@@ -1341,15 +1384,6 @@ namespace Emby.Server.Implementations.Library
return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
}
/// <inheritdoc />
public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
// Ensure the location is available.
Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
}
/// <summary>
/// Reloads the root media folder.
/// </summary>
@@ -1476,6 +1510,10 @@ namespace Emby.Server.Implementations.Library
var numComplete = 0;
var numTasks = tasks.Count;
_logger.LogInformation("Running {TaskCount} post-scan task(s)", numTasks);
var phaseStart = Stopwatch.GetTimestamp();
foreach (var task in tasks)
{
// Prevent access to modified closure
@@ -1493,20 +1531,45 @@ namespace Emby.Server.Implementations.Library
progress.Report(innerPercent);
});
_logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
var taskName = task.GetType().Name;
var taskStart = Stopwatch.GetTimestamp();
_logger.LogInformation(
"Running post-scan task {TaskNumber}/{TaskCount}: {TaskName}",
currentNumComplete + 1,
numTasks,
taskName);
try
{
await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
var elapsed = Stopwatch.GetElapsedTime(taskStart);
_logger.LogInformation(
"Post-scan task {TaskName} completed after {Minutes} minute(s) and {Seconds} seconds",
taskName,
Math.Truncate(elapsed.TotalMinutes),
elapsed.Seconds);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
var elapsed = Stopwatch.GetElapsedTime(taskStart);
_logger.LogInformation(
"Post-scan task {TaskName} cancelled after {Minutes} minute(s) and {Seconds} seconds",
taskName,
Math.Truncate(elapsed.TotalMinutes),
elapsed.Seconds);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error running post-scan task");
var elapsed = Stopwatch.GetElapsedTime(taskStart);
_logger.LogError(
ex,
"Post-scan task {TaskName} failed after {Minutes} minute(s) and {Seconds} seconds",
taskName,
Math.Truncate(elapsed.TotalMinutes),
elapsed.Seconds);
}
numComplete++;
@@ -1515,6 +1578,12 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100);
}
var phaseElapsed = Stopwatch.GetElapsedTime(phaseStart);
_logger.LogInformation(
"All post-scan tasks completed after {Minutes} minute(s) and {Seconds} seconds",
Math.Truncate(phaseElapsed.TotalMinutes),
phaseElapsed.Seconds);
_persistenceService.UpdateInheritedValues();
progress.Report(100);
@@ -1732,9 +1801,21 @@ namespace Emby.Server.Implementations.Library
return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
}
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
/// <inheritdoc/>
public Dictionary<Guid, ItemCounts> GetItemCountsForNameItems(BaseItemKind kind, IReadOnlyList<Guid> ids, BaseItemKind[] relatedItemKinds, User? user)
{
return _countService.GetChildCountBatch(parentIds, userId);
var query = new InternalItemsQuery(user);
if (user is not null)
{
AddUserToQuery(query, user);
}
return _countService.GetItemCountsForNameItems(kind, ids, relatedItemKinds, query);
}
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user)
{
return _countService.GetChildCountBatch(parentIds, user);
}
/// <inheritdoc/>
@@ -1901,14 +1982,14 @@ namespace Emby.Server.Implementations.Library
}
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
query.AncestorIds = [];
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
var topParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
if (topParentIds.Length == 0)
{
query.TopParentIds = [Guid.NewGuid()];
return;
}
query.TopParentIds = topParentIds;
query.AncestorIds = [];
}
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
@@ -1954,12 +2035,15 @@ namespace Emby.Server.Implementations.Library
if (parents.All(i => i is ICollectionFolder || i is UserView))
{
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
var topParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
if (topParentIds.Length > 0)
{
query.TopParentIds = [Guid.NewGuid()];
query.TopParentIds = topParentIds;
}
else
{
SetAncestorIds(query, parents);
}
}
else if (parents.Count == 1 && parents.First() is Folder folder
@@ -1968,34 +2052,31 @@ namespace Emby.Server.Implementations.Library
{
// Playlists and BoxSets store their contents in LinkedChildren and never
// populate AncestorIds for those items, so a recursive AncestorIds query
// would return zero rows. Resolve to the linked child IDs up front and
// route through the existing indexed ItemIds filter.
query.ItemIds = folder.LinkedChildren
.Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
.Select(lc => lc.ItemId!.Value)
.ToArray();
// Empty linked-children should still return empty rather than scanning everything.
if (query.ItemIds.Length == 0)
{
query.ItemIds = [Guid.NewGuid()];
}
// would return zero rows. Filter by the descendant set instead, which follows
// the links and keeps descending, so a linked folder contributes what is below
// it as well - the episodes of a Series added to a collection, for example.
query.DescendantOfId = folder.Id;
}
else
{
// We need to be able to query from any arbitrary ancestor up the tree
query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
// Prevent searching in all libraries due to empty filter
if (query.AncestorIds.Length == 0)
{
query.AncestorIds = [Guid.NewGuid()];
}
SetAncestorIds(query, parents);
}
query.Parent = null;
}
private static void SetAncestorIds(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
{
// We need to be able to query from any arbitrary ancestor up the tree
query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
// Prevent searching in all libraries due to empty filter
if (query.AncestorIds.Length == 0)
{
query.AncestorIds = [Guid.NewGuid()];
}
}
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
{
if (query.User is null)
@@ -2234,6 +2315,12 @@ namespace Emby.Server.Implementations.Library
return [];
}
/// <inheritdoc />
public IReadOnlySet<Guid> GetItemIdsWithAlternateVersions(IReadOnlyList<Guid> itemIds)
{
return _linkedChildrenService.GetItemIdsWithAlternateVersions(itemIds);
}
/// <inheritdoc />
public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType childType)
{
@@ -2376,6 +2463,7 @@ namespace Emby.Server.Implementations.Library
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
altVideo.IsInMixedFolder = video.IsInMixedFolder;
// ResolveAlternateVersion only sees the alternate's primary file.
// If the alternate is itself a stack (e.g. 1080p part1 + part2),
// detect its parts from sibling files so its AdditionalParts persist.
@@ -2499,9 +2587,15 @@ namespace Emby.Server.Implementations.Library
}
}
if (!File.Exists(image.Path))
if (string.IsNullOrEmpty(image.Path) || !File.Exists(image.Path))
{
_logger.LogWarning("Image not found at {ImagePath}", image.Path);
_logger.LogWarning(
"{ImageType} image for {ItemName} ({ItemId}) not found at \"{ImagePath}\", source was {SourcePath}",
img.Type,
item.Name,
item.Id,
image.Path,
img.Path);
continue;
}
@@ -2561,6 +2655,8 @@ namespace Emby.Server.Implementations.Library
item.DateLastSaved = DateTime.UtcNow;
}
ForgetDroppedLocalAlternateVersions(items);
// Resolve and add any local alternate version items that don't exist yet
// This ensures they exist in the database when LinkedChildren are processed
var allItems = new List<BaseItem>(items);
@@ -2589,6 +2685,7 @@ namespace Emby.Server.Implementations.Library
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
altVideo.IsInMixedFolder = video.IsInMixedFolder;
// ResolveAlternateVersion only sees the alternate's primary file.
// If the alternate is itself a stack (e.g. 1080p part1 + part2),
// detect its parts from sibling files so its AdditionalParts persist.
@@ -2649,6 +2746,30 @@ namespace Emby.Server.Implementations.Library
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
/// <summary>
/// Forgets the cached local alternate versions of the supplied items that they no longer list.
/// </summary>
/// <param name="items">The items about to be saved.</param>
private void ForgetDroppedLocalAlternateVersions(IReadOnlyList<BaseItem> items)
{
foreach (var video in items.OfType<Video>())
{
var videoType = video.GetType();
var keptIds = video.LocalAlternateVersions
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => GetNewItemId(path, videoType))
.ToHashSet();
foreach (var versionId in GetLocalAlternateVersionIds(video))
{
if (!keptIds.Contains(versionId))
{
_cache.TryRemove(versionId, out _);
}
}
}
}
/// <inheritdoc />
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
{
@@ -2872,7 +2993,8 @@ namespace Emby.Server.Implementations.Library
"views",
_fileSystem.GetValidFilename(viewType.ToString()));
var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
// The display name is localized, so it must not take part in the id.
var id = GetNewItemId(path + "_namedview_" + viewType.ToString(), typeof(UserView));
var item = GetItemById(id) as UserView;
@@ -2896,6 +3018,13 @@ namespace Emby.Server.Implementations.Library
refresh = true;
}
else if (!string.Equals(item.Name, name, StringComparison.Ordinal))
{
item.Name = name;
item.ForcedSortName = sortName;
refresh = true;
}
if (refresh)
{
@@ -2916,7 +3045,9 @@ namespace Emby.Server.Implementations.Library
var parentIdString = parentId.IsEmpty()
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
// The name is either localized (grouped views) or the library folder's own name.
var idValues = "38_namedview_" + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
var id = GetNewItemId(idValues, typeof(UserView));
@@ -2946,6 +3077,11 @@ namespace Emby.Server.Implementations.Library
isNew = true;
}
else if (!string.Equals(item.Name, name, StringComparison.Ordinal))
{
item.Name = name;
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
var lastRefreshedUtc = item.DateLastRefreshed;
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
@@ -3047,7 +3183,7 @@ namespace Emby.Server.Implementations.Library
var parentIdString = parentId.IsEmpty()
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
var idValues = "37_namedview_" + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
if (!string.IsNullOrEmpty(uniqueId))
{
idValues += uniqueId;
@@ -3081,9 +3217,10 @@ namespace Emby.Server.Implementations.Library
isNew = true;
}
if (viewType != item.ViewType)
if (viewType != item.ViewType || !string.Equals(item.Name, name, StringComparison.Ordinal))
{
item.ViewType = viewType;
item.Name = name;
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
@@ -3503,12 +3640,24 @@ namespace Emby.Server.Implementations.Library
return _peopleRepository.GetPeopleNames(query);
}
/// <inheritdoc/>
public int DeleteOrphanedCredits()
{
return _peopleRepository.DeleteOrphanedCredits();
}
/// <inheritdoc/>
public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes)
{
return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes);
}
/// <inheritdoc/>
public IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds)
{
return _peopleRepository.GetPeopleByItems(itemIds);
}
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
{
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
@@ -3542,7 +3691,20 @@ namespace Emby.Server.Implementations.Library
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return item.GetImageInfo(image.Type, imageIndex);
var localImage = item.GetImageInfo(image.Type, imageIndex);
if (localImage is null)
{
throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture,
"Downloaded {0} image {1} from {2} is not attached to {3} ({4})",
image.Type,
imageIndex,
url,
item.Name,
item.Id));
}
return localImage;
}
catch (HttpRequestException ex)
{
@@ -3564,7 +3726,13 @@ namespace Emby.Server.Implementations.Library
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
}
throw new InvalidOperationException("Unable to convert any images to local");
throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture,
"Unable to convert any {0} image url in \"{1}\" to a local file for {2} ({3})",
image.Type,
image.Path,
item.Name,
item.Id));
}
public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
@@ -3620,6 +3788,10 @@ namespace Emby.Server.Implementations.Library
AddMediaPathInternal(name, path, false);
}
}
// The libraries root was listed before this folder existed, so drop that listing:
// anything still reading it resolves the library set without the new folder.
_directoryService.Invalidate(virtualFolderPath);
}
finally
{
@@ -3646,27 +3818,14 @@ namespace Emby.Server.Implementations.Library
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
var createEntity = false;
var personEntity = GetPerson(person.Name);
if (personEntity is null)
{
try
{
var path = Person.GetPath(person.Name);
var info = Directory.CreateDirectory(path);
personEntity = new Person()
{
Name = person.Name,
Id = GetItemByNameId<Person>(path),
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
Path = path
};
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
personEntity = GetOrCreatePerson(person.Name);
saveEntity = true;
createEntity = true;
}
catch (Exception ex)
{
@@ -3700,11 +3859,6 @@ namespace Emby.Server.Implementations.Library
if (saveEntity)
{
if (createEntity)
{
CreateItems([personEntity], null, CancellationToken.None);
}
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
personEntity.DateLastSaved = DateTime.UtcNow;
@@ -3744,7 +3898,9 @@ namespace Emby.Server.Implementations.Library
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName)
?? throw new FileNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
CreateShortcut(virtualFolderPath, pathInfo);
@@ -3765,7 +3921,9 @@ namespace Emby.Server.Implementations.Library
ArgumentNullException.ThrowIfNull(mediaPath);
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName)
?? throw new FileNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
@@ -3804,9 +3962,9 @@ namespace Emby.Server.Implementations.Library
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var path = Path.Combine(rootFolderPath, name);
var path = FileSystemHelper.GetChildPath(rootFolderPath, name);
if (!Directory.Exists(path))
if (path is null || !Directory.Exists(path))
{
throw new FileNotFoundException("The media folder does not exist");
}
@@ -3816,6 +3974,7 @@ namespace Emby.Server.Implementations.Library
try
{
Directory.Delete(path, true);
_directoryService.Invalidate(path);
}
finally
{
@@ -3870,9 +4029,9 @@ namespace Emby.Server.Implementations.Library
ArgumentException.ThrowIfNullOrEmpty(mediaPath);
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName);
if (!Directory.Exists(virtualFolderPath))
if (virtualFolderPath is null || !Directory.Exists(virtualFolderPath))
{
throw new FileNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
@@ -3885,6 +4044,7 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(shortcut))
{
_fileSystem.DeleteFile(shortcut);
_directoryService.Invalidate(shortcut);
}
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
@@ -3928,6 +4088,7 @@ namespace Emby.Server.Implementations.Library
}
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
_directoryService.Invalidate(lnk);
RemoveContentTypeOverrides(path);
}
@@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
args.LibraryOptions.SeasonZeroDisplayName :
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameSeasonNumber"),
_localization.GetServerLocalizedString("NameSeasonNumber"),
seasonNumber,
args.LibraryOptions.PreferredMetadataLanguage);
}
@@ -129,6 +129,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var tmdbId = justName.GetAttributeValue("tmdbid");
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
// Anime databases model a single cour as its own entry, so a multi-season
// series maps to one of these ids per season rather than one per series.
var anidbId = justName.GetAttributeValue("anidbid");
item.TrySetProviderId("AniDB", anidbId);
var aniListId = justName.GetAttributeValue("anilistid");
item.TrySetProviderId("AniList", aniListId);
var aniSearchId = justName.GetAttributeValue("anisearchid");
item.TrySetProviderId("AniSearch", aniSearchId);
}
}
}
@@ -92,49 +92,45 @@ public class SearchManager : ISearchManager
await Task.WhenAll(externalTask, internalTask).ConfigureAwait(false);
var externalResults = await externalTask.ConfigureAwait(false);
var fromExternal = externalResults.Count > 0;
IReadOnlyList<SearchResult> results;
if (fromExternal)
{
results = externalResults;
}
else
{
results = await internalTask.ConfigureAwait(false);
if (_internalProviders.Length > 0)
{
_logger.LogDebug("No results from external providers, using internal provider results");
}
}
// Internal providers apply user-access filtering inline in their queries. External
// providers don't know about user permissions, so they may return IDs from hidden
// libraries or items the user is otherwise blocked from. Run the post-filter only
// when results came from externals to close that gap. The Items controller's second
// roundtrip via folder.GetItems applies most of these again, but it does not restrict
// by TopParentIds when ItemIds is set.
if (fromExternal && results.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
// libraries or items the user is otherwise blocked from. Filter them here to close
// that gap. The Items controller's second roundtrip via folder.GetItems applies most
// of these again, but it does not restrict by TopParentIds when ItemIds is set.
if (externalResults.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
{
var user = _userManager.GetUserById(query.UserId.Value);
if (user is not null)
{
results = await FilterByUserAccessAsync(results, user, cancellationToken).ConfigureAwait(false);
externalResults = await FilterByUserAccessAsync(externalResults, user, query, cancellationToken).ConfigureAwait(false);
}
}
return results;
if (externalResults.Count > 0)
{
return externalResults;
}
if (_internalProviders.Length > 0)
{
_logger.LogDebug("No results from external providers, using internal provider results");
}
return await internalTask.ConfigureAwait(false);
}
private async Task<IReadOnlyList<SearchResult>> FilterByUserAccessAsync(
IReadOnlyList<SearchResult> candidates,
User user,
SearchProviderQuery query,
CancellationToken cancellationToken)
{
// SetUser populates parental rating + blocked/allowed tags. ConfigureUserAccess populates
// TopParentIds for the user's accessible libraries — we call it before assigning ItemIds
// because LibraryManager.AddUserToQuery skips TopParentIds when ItemIds is non-empty.
var accessFilter = new InternalItemsQuery(user);
_libraryManager.ConfigureUserAccess(accessFilter, user);
// SetUser populates parental rating + blocked/allowed tags, Build populates TopParentIds
// for the user's accessible libraries. The candidate ids are applied to the query below
// rather than to the filter because LibraryManager.AddUserToQuery skips TopParentIds when
// ItemIds is non-empty.
var accessFilter = SearchQueryAccessFilter.Build(user, query, _libraryManager);
Guid[] candidateIds = [.. candidates.Select(c => c.ItemId)];
@@ -147,17 +143,16 @@ public class SearchManager : ISearchManager
baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, accessFilter);
var allowedCount = await baseQuery.CountAsync(cancellationToken).ConfigureAwait(false);
if (allowedCount == candidates.Count)
{
return candidates;
}
var allowedIds = await baseQuery
.Select(e => e.Id)
.ToHashSetAsync(cancellationToken)
.ConfigureAwait(false);
if (allowedIds.Count == candidates.Count)
{
return candidates;
}
var filtered = candidates.Where(c => allowedIds.Contains(c.ItemId)).ToList();
if (filtered.Count < candidates.Count)
{
@@ -0,0 +1,38 @@
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
namespace Emby.Server.Implementations.Library.Search;
/// <summary>
/// Builds the access filter that decides which items a search may return for a user.
/// </summary>
internal static class SearchQueryAccessFilter
{
/// <summary>
/// Builds an access filter carrying the search's library access and type filters.
/// </summary>
/// <param name="user">The user the search runs for.</param>
/// <param name="query">The search query.</param>
/// <param name="libraryManager">The library manager.</param>
/// <returns>The access filter.</returns>
public static InternalItemsQuery Build(User user, SearchProviderQuery query, ILibraryManager libraryManager)
{
// The type filters have to travel with the access filter: a by-name item belongs to no
// library, so it carries no TopParentId to match, and the library filter only knows to
// exempt it when the query says those types are wanted. A search scoped to a parent gets
// no exemption because a by-name item has no parent to descend from either.
var accessFilter = new InternalItemsQuery(user)
{
IncludeItemTypes = query.IncludeItemTypes,
ExcludeItemTypes = query.ExcludeItemTypes,
IncludeItemsByName = !query.ParentId.HasValue || query.ParentId.Value.IsEmpty()
};
// ConfigureUserAccess populates TopParentIds for the libraries the user may open.
libraryManager.ConfigureUserAccess(accessFilter, user);
return accessFilter;
}
}
@@ -114,7 +114,7 @@ public class SqlSearchProvider : IInternalSearchProvider
dbQuery = ApplyTypeFilter(dbQuery, query.IncludeItemTypes, query.ExcludeItemTypes);
dbQuery = ApplyMediaTypeFilter(dbQuery, query.MediaTypes);
dbQuery = ApplyParentFilter(dbQuery, query.ParentId);
dbQuery = ApplyUserAccessFilter(dbContext, dbQuery, query.UserId);
dbQuery = ApplyUserAccessFilter(dbContext, dbQuery, query);
// Compute the score in SQL: the ternary translates to a CASE WHEN. CleanName is
// the pre-normalized (lowercase, diacritic-stripped) form, so we score against it
@@ -196,8 +196,9 @@ public class SqlSearchProvider : IInternalSearchProvider
private IQueryable<BaseItemEntity> ApplyUserAccessFilter(
JellyfinDbContext dbContext,
IQueryable<BaseItemEntity> query,
Guid? userId)
SearchProviderQuery searchQuery)
{
var userId = searchQuery.UserId;
if (!userId.HasValue || userId.Value.IsEmpty())
{
return query;
@@ -209,8 +210,7 @@ public class SqlSearchProvider : IInternalSearchProvider
return query;
}
var accessFilter = new InternalItemsQuery(user);
_libraryManager.ConfigureUserAccess(accessFilter, user);
var accessFilter = SearchQueryAccessFilter.Build(user, searchQuery, _libraryManager);
return _queryHelpers.ApplyAccessFiltering(dbContext, query, accessFilter);
}
@@ -260,7 +260,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
}
var candidateRows = await context.ItemValuesMap.AsNoTracking()
.Where(m => m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue))
.Where(m => !m.Item.PrimaryVersionId.HasValue && m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue))
.Select(m => new { m.ItemId, Key = m.ItemValue.CleanValue })
.ToListAsync(cancellationToken).ConfigureAwait(false);
@@ -276,6 +276,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
if (personSourceRows.Count > 0)
{
var personCandidateRows = await context.PeopleBaseItemMap.AsNoTracking()
.Where(m => !m.Item.PrimaryVersionId.HasValue)
.Where(m => context.PeopleBaseItemMap
.Where(s => sourceIds.Contains(s.ItemId) && _scoredPersonTypes.Contains(s.People.PersonType))
.Select(s => s.PeopleId)
@@ -0,0 +1,42 @@
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Builds the access filter that decides which items a similar-items lookup may return for a user.
/// </summary>
internal static class SimilarItemsAccessFilter
{
private static readonly BaseItemKind[] _itemByNameKinds =
[
BaseItemKind.Person,
BaseItemKind.Genre,
BaseItemKind.MusicGenre,
BaseItemKind.MusicArtist,
BaseItemKind.Studio
];
/// <summary>
/// Builds an access filter carrying the user's library access and parental restrictions.
/// </summary>
/// <param name="user">The user the lookup runs for.</param>
/// <param name="libraryManager">The library manager.</param>
/// <returns>The access filter.</returns>
public static InternalItemsQuery Build(User user, ILibraryManager libraryManager)
{
// IncludeItemTypes is read only for the by-name exemption here; the caller applies this
// filter through ApplyAccessFiltering, which does not translate it into a type restriction.
var accessFilter = new InternalItemsQuery(user)
{
IncludeItemTypes = _itemByNameKinds
};
// ConfigureUserAccess populates TopParentIds for the libraries the user may open.
libraryManager.ConfigureUserAccess(accessFilter, user);
return accessFilter;
}
}
@@ -7,8 +7,10 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -16,11 +18,13 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.SimilarItems;
@@ -35,6 +39,8 @@ public class SimilarItemsManager : ISimilarItemsManager
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IItemQueryHelpers _queryHelpers;
private ISimilarItemsProvider[] _similarItemsProviders = [];
/// <summary>
@@ -45,18 +51,24 @@ public class SimilarItemsManager : ISimilarItemsManager
/// <param name="libraryManager">The library manager.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
/// <param name="dbProvider">The database context factory.</param>
/// <param name="queryHelpers">The shared item query helpers.</param>
public SimilarItemsManager(
ILogger<SimilarItemsManager> logger,
IServerApplicationPaths appPaths,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager serverConfigurationManager)
IServerConfigurationManager serverConfigurationManager,
IDbContextFactory<JellyfinDbContext> dbProvider,
IItemQueryHelpers queryHelpers)
{
_logger = logger;
_appPaths = appPaths;
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_serverConfigurationManager = serverConfigurationManager;
_dbProvider = dbProvider;
_queryHelpers = queryHelpers;
}
/// <inheritdoc/>
@@ -183,6 +195,7 @@ public class SimilarItemsManager : ISimilarItemsManager
// Collect references in batches and resolve against local library.
// Stop fetching once we have enough resolved local items.
const int BatchSize = 20;
const int MaxRemoteReferenceFetchLimit = 500;
var remaining = requestedLimit - allResults.Count;
var collectedReferences = new List<SimilarItemReference>();
var pendingBatch = new List<SimilarItemReference>();
@@ -199,7 +212,7 @@ public class SimilarItemsManager : ISimilarItemsManager
remaining -= resolvedItems.Count;
pendingBatch.Clear();
if (remaining <= 0)
if (remaining <= 0 || collectedReferences.Count >= MaxRemoteReferenceFetchLimit)
{
break;
}
@@ -229,11 +242,64 @@ public class SimilarItemsManager : ISimilarItemsManager
}
}
return allResults
var ordered = allResults
.OrderByDescending(x => x.Score)
.Select(x => x.Item)
.Take(requestedLimit)
.ToList();
return await FilterByLibraryAccessAsync(ordered, user, cancellationToken).ConfigureAwait(false);
}
private async Task<IReadOnlyList<BaseItem>> FilterByLibraryAccessAsync(
IReadOnlyList<BaseItem> candidates,
User? user,
CancellationToken cancellationToken)
{
if (candidates.Count == 0 || user is null)
{
return candidates;
}
var accessFilter = SimilarItemsAccessFilter.Build(user, _libraryManager);
// No accessible libraries means nothing to compare against, and an empty TopParentIds set
// would disable the filter rather than reject everything.
if (accessFilter.TopParentIds.Length == 0)
{
return candidates;
}
Guid[] candidateIds = [.. candidates.Select(c => c.Id)];
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var baseQuery = dbContext.BaseItems
.AsNoTracking()
.WhereOneOrMany(candidateIds, e => e.Id);
baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, accessFilter);
var allowedCount = await baseQuery.CountAsync(cancellationToken).ConfigureAwait(false);
if (allowedCount == candidates.Count)
{
return candidates;
}
var allowedIds = await baseQuery
.Select(e => e.Id)
.ToHashSetAsync(cancellationToken)
.ConfigureAwait(false);
var filtered = candidates.Where(c => allowedIds.Contains(c.Id)).ToList();
_logger.LogDebug(
"Dropped {Dropped} of {Total} similar-item candidates due to user access filtering",
candidates.Count - filtered.Count,
candidates.Count);
return filtered;
}
}
/// <inheritdoc/>
@@ -375,19 +441,39 @@ public class SimilarItemsManager : ISimilarItemsManager
var batchResults = await batchProvider.GetBatchSimilarItemsAsync(baselineItems, query, cancellationToken).ConfigureAwait(false);
// Filter once across every category rather than per baseline, so a batch provider costs one
// access query no matter how many categories it produced.
var allItems = batchResults.Values.SelectMany(items => items).DistinctBy(item => item.Id).ToList();
var allowed = await FilterByLibraryAccessAsync(allItems, query.User, cancellationToken).ConfigureAwait(false);
HashSet<Guid>? allowedIds = allowed.Count == allItems.Count
? null
: [.. allowed.Select(item => item.Id)];
var recommendations = new List<SimilarItemsRecommendation>(baselineItems.Count);
foreach (var baseline in baselineItems)
{
if (batchResults.TryGetValue(baseline.Id, out var similar) && similar.Count > 0)
if (!batchResults.TryGetValue(baseline.Id, out var similar) || similar.Count == 0)
{
recommendations.Add(new SimilarItemsRecommendation
{
BaselineItemName = baseline.Name,
CategoryId = baseline.Id,
RecommendationType = recommendationType,
Items = similar
});
continue;
}
if (allowedIds is not null)
{
similar = similar.Where(item => allowedIds.Contains(item.Id)).ToList();
if (similar.Count == 0)
{
continue;
}
}
recommendations.Add(new SimilarItemsRecommendation
{
BaselineItemName = baseline.Name,
CategoryId = baseline.Id,
RecommendationType = recommendationType,
Items = similar
});
}
return recommendations;
@@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library
if (_config.Configuration.EnableFolderView)
{
var name = _localizationManager.GetLocalizedString("Folders");
var name = _localizationManager.GetServerLocalizedString("Folders");
list.Add(_libraryManager.GetNamedView(name, CollectionType.folders, string.Empty));
}
@@ -168,7 +168,7 @@ namespace Emby.Server.Implementations.Library
public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName)
{
var name = _localizationManager.GetLocalizedString(localizationKey);
var name = _localizationManager.GetServerLocalizedString(localizationKey);
return GetUserSubViewWithName(name, parentId, type, sortName);
}
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library
return GetUserView((Folder)parents[0], viewType, string.Empty);
}
var name = _localizationManager.GetLocalizedString(localizationKey);
var name = _localizationManager.GetServerLocalizedString(localizationKey);
return _libraryManager.GetNamedView(user, name, viewType, sortName);
}
@@ -396,6 +396,12 @@ namespace Emby.Server.Implementations.Library
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.movies);
}
if (collectionType is null)
{
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.unknown);
}
}
return _libraryManager.GetItemList(query, parents);
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
@@ -61,6 +62,9 @@ public class ArtistsValidator
var count = names.Count;
var refreshed = 0;
var liveIds = new HashSet<Guid>();
var unresolved = 0;
foreach (var name in names)
{
try
@@ -73,13 +77,20 @@ public class ArtistsValidator
// Fall back to GetArtist if not found (creates new item if needed)
item ??= _libraryManager.GetArtist(name);
var isNew = !existingArtistIds.Contains(item.Id);
var neverRefreshed = item.DateLastRefreshed == default;
if (isNew || neverRefreshed)
// A name with no item is nothing to refresh, and nothing to keep alive either.
if (item is not null)
{
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
liveIds.Add(item.Id);
var isNew = !existingArtistIds.Contains(item.Id);
var neverRefreshed = item.DateLastRefreshed == default;
if (isNew || neverRefreshed)
{
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
}
}
}
catch (OperationCanceledException)
@@ -88,6 +99,7 @@ public class ArtistsValidator
}
catch (Exception ex)
{
unresolved++;
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
}
@@ -101,13 +113,26 @@ public class ArtistsValidator
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new artists out of {TotalCount} total", refreshed, count);
// Every name that threw is a name whose artist is missing from the live set, and deleting against
// a live set with holes in it deletes artists the library still refers to. Leave the sweep to a
// run that got a clean read of them.
if (unresolved > 0)
{
_logger.LogWarning(
"Not removing dead artists: {Count} of {TotalCount} names could not be resolved this run",
unresolved,
count);
progress.Report(100);
return;
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.MusicArtist],
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>()
.Where(item => item.IsAccessedByName)
}).OfType<MusicArtist>()
.Where(item => item.IsAccessedByName && !liveIds.Contains(item.Id))
.ToList();
foreach (var item in deadEntities)
@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
@@ -17,104 +17,143 @@ namespace Emby.Server.Implementations.Library.Validators;
public class PeopleValidator
{
/// <summary>
/// The _library manager.
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The _logger.
/// The logger.
/// </summary>
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
private readonly ILogger<PeopleValidator> _logger;
/// <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)
public PeopleValidator(ILibraryManager libraryManager, ILogger<PeopleValidator> logger)
{
_libraryManager = libraryManager;
_logger = logger;
_fileSystem = fileSystem;
}
/// <summary>
/// Validates the people.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
// Before the refresh below walks them: a credit no item maps to any more stands for nothing,
// and while it is there the person it names cannot reach the dead-person sweep either.
var numOrphaned = _libraryManager.DeleteOrphanedCredits();
if (numOrphaned > 0)
{
_logger.LogInformation("Deleted {Amount} credits no item maps to", numOrphaned);
}
var names = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
var existingPersonIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Person]
}).ToHashSet();
var (newNames, deadIds) = PartitionCreditsByPersonId(names, _libraryManager.GetPersonId, existingPersonIds);
var numComplete = 0;
var count = names.Count;
var refreshed = 0;
var numPeople = people.Count;
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
_logger.LogDebug("Will refresh {Amount} people", numPeople);
foreach (var person in people)
foreach (var name in names)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var item = _libraryManager.GetPerson(person);
if (item is null)
var item = _libraryManager.GetOrCreatePerson(name);
var isNew = !existingPersonIds.Contains(item.Id);
var neverRefreshed = item.DateLastRefreshed == default;
if (isNew || neverRefreshed)
{
_logger.LogWarning("Failed to get person: {Name}", person);
continue;
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
}
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
};
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
_logger.LogError(ex, "Error refreshing {PersonName}", name);
}
// Update progress
numComplete++;
double percent = numComplete;
percent /= numPeople;
percent /= count;
percent *= 100;
subProgress.Report(100 * percent);
progress.Report(percent);
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Person],
IsDeadPerson = true,
IsLocked = false
});
_logger.LogInformation(
"Refreshed metadata for {RefreshedCount} people out of {TotalCount} total, {NewCount} of which had no item yet",
refreshed,
count,
newNames.Count);
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
// A person somebody locked is theirs, not ours, however little the library still credits them.
var deadEntities = deadIds
.Select(_libraryManager.GetItemById)
.OfType<Person>()
.Where(item => !item.IsLocked)
.ToList();
var i = 0;
foreach (var item in deadEntities.Chunk(500))
foreach (var item in deadEntities)
{
_libraryManager.DeleteItemsUnsafeFast(item, true);
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
}
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
progress.Report(100);
}
_logger.LogInformation("People validation complete");
/// <summary>
/// Splits the person items into the ones a credit still calls for and the ones nothing does.
/// </summary>
/// <param name="creditNames">Every name credited on an item, from the people table.</param>
/// <param name="getPersonId">Maps a credit name to the id its person item has.</param>
/// <param name="existingPersonIds">The ids of the person items that exist.</param>
/// <returns>The credits needing an item, and the ids of the items nothing credits.</returns>
internal static (List<string> NewNames, List<Guid> DeadIds) PartitionCreditsByPersonId(
IReadOnlyList<string> creditNames,
Func<string, Guid> getPersonId,
IReadOnlySet<Guid> existingPersonIds)
{
ArgumentNullException.ThrowIfNull(creditNames);
ArgumentNullException.ThrowIfNull(getPersonId);
ArgumentNullException.ThrowIfNull(existingPersonIds);
var newNames = new List<string>();
var liveIds = new HashSet<Guid>();
foreach (var name in creditNames)
{
var personId = getPersonId(name);
// Distinct credit names can normalize onto one id; only the first of them needs an item.
if (liveIds.Add(personId) && !existingPersonIds.Contains(personId))
{
newNames.Add(name);
}
}
var deadIds = existingPersonIds.Where(id => !liveIds.Contains(id)).ToList();
return (newNames, deadIds);
}
}
@@ -100,7 +100,7 @@
"TaskAudioNormalization": "تطبيع الصوت",
"TaskAudioNormalizationDescription": "يفحص الملفات لجمع بيانات تطبيع الصوت.",
"TaskDownloadMissingLyrics": "تنزيل الكلمات المفقودة",
"TaskDownloadMissingLyricsDescription": "ينزّل الكلمات للأغاني.",
"TaskDownloadMissingLyricsDescription": "تحميل كلمات الأغاني",
"TaskExtractMediaSegments": "فحص مقاطع المحتوى",
"TaskExtractMediaSegmentsDescription": "يستخرج أو يحصل على مقاطع المحتوى من الملحقات المفعّلة لمقاطع المحتوى (MediaSegment).",
"TaskMoveTrickplayImages": "نقل موقع صور معاينات التنقل",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
"CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل.",
"Original": "فريد",
"LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}"
"LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}",
"NameExtraBehindTheScenes": "خلف المشاهد",
"NameExtraClip": "مقطع",
"NameExtraDeletedScene": "المشهد المحذوف",
"NameExtraInterview": "مقابلة",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "عيّنة",
"NameExtraScene": "مشهد",
"NameExtraShort": "قصير",
"NameExtraThemeSong": "الاغنية السمة",
"NameExtraThemeVideo": "الفيديو السمة",
"NameExtraFeaturette": "فيلم قصير إضافي",
"NameExtraTrailer": "إعلان ترويجي",
"NameExtraUnknown": "إضافي"
}
@@ -106,5 +106,17 @@
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
"CleanupUserDataTask": "Задача па ачыстцы даных карыстальніка",
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён.",
"LyricDownloadFailureFromForItem": "Не ўдалося загрузіць тэкст песні з {0} для {1}",
"NameExtraDeletedScene": "Выдаленая сцэна",
"NameExtraInterview": "Інтэрв'ю",
"NameExtraNumbered": "{0} {1}",
"NameExtraScene": "Сцэна",
"NameExtraTrailer": "Трэйлер",
"NameExtraBehindTheScenes": "За кулісамі",
"NameExtraClip": "Кліп",
"NameExtraFeaturette": "Кароткаметражка",
"NameExtraSample": "Прыклад",
"NameExtraShort": "Кароткаметражка",
"NameExtraThemeSong": "Тэматычная песня"
}
@@ -106,5 +106,20 @@
"TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
"TaskExtractMediaSegments": "Сканиране за сегменти",
"CleanupUserDataTask": "Задача за почистване на потребителски данни",
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни."
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни.",
"LyricDownloadFailureFromForItem": "Текстът на песента не успя да се изтегли от {0} за {1}",
"NameExtraBehindTheScenes": "Зад кулисите",
"NameExtraScene": "Сцена",
"NameExtraShort": "Откъс",
"NameExtraThemeVideo": "Тематично видео",
"NameExtraTrailer": "Трейлър",
"NameExtraUnknown": "Екстра",
"NameExtraClip": "Клип",
"NameExtraDeletedScene": "Изтрита Сцена",
"NameExtraFeaturette": "Кратък филм",
"NameExtraInterview": "Интервю",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Пример",
"NameExtraThemeSong": "Тема-песен",
"Original": "Оригинал"
}
@@ -106,5 +106,17 @@
"TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay",
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana."
"CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana.",
"NameExtraBehindTheScenes": "Iza kulisa",
"NameExtraClip": "Isječak",
"NameExtraDeletedScene": "Izbrišana scena",
"NameExtraFeaturette": "Kratki prilog",
"NameExtraInterview": "Intervju",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Uzorak",
"NameExtraScene": "Scena",
"NameExtraShort": "Kratko",
"NameExtraThemeSong": "Tema",
"NameExtraThemeVideo": "Tematski video",
"NameExtraTrailer": "Najava"
}
@@ -108,5 +108,18 @@
"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",
"Original": "Originál",
"LyricDownloadFailureFromForItem": "Nepodařilo se stáhnout texty pro {1} ze služby {0}"
"LyricDownloadFailureFromForItem": "Nepodařilo se stáhnout texty pro {1} ze služby {0}",
"NameExtraBehindTheScenes": "Zákulisí",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Vymazaná scéna",
"NameExtraFeaturette": "Featurette",
"NameExtraInterview": "Rozhovor",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Ukázka",
"NameExtraScene": "Scéna",
"NameExtraShort": "Krátké",
"NameExtraThemeSong": "Úvodní píseň",
"NameExtraThemeVideo": "Úvodní video",
"NameExtraTrailer": "Upoutávka",
"NameExtraUnknown": "Extra"
}
@@ -51,7 +51,7 @@
"Shows": "Serier",
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
"TvShows": "TV-serier",
"TvShows": "Serier",
"UserCreatedWithName": "Bruger {0} er blevet oprettet",
"UserDeletedWithName": "Brugeren {0} er nu slettet",
"UserDownloadingItemWithValues": "{0} henter {1}",
@@ -80,8 +80,8 @@
"TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
"TaskRefreshChannelsDescription": "Opdaterer information for internetkanaler.",
"TaskRefreshChannels": "Opdatér kanaler",
"TaskCleanTranscodeDescription": "Fjerner omkodningsfiler, som er mere end 1 dag gamle.",
"TaskCleanTranscode": "Tøm omkodningsmappen",
"TaskCleanTranscodeDescription": "Fjerner transkodningsfiler, som er mere end 1 dag gamle.",
"TaskCleanTranscode": "Tøm transkodningsmappen",
"TaskRefreshPeople": "Opdatér personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Brugerdata oprydningsopgave",
"CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage.",
"LyricDownloadFailureFromForItem": "Sangtekster kunne ikke downloades fra {0} til {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Bag scenerne",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Slettet scene",
"NameExtraFeaturette": "Featurette",
"NameExtraInterview": "Interview",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Smagsprøve",
"NameExtraScene": "Scene",
"NameExtraShort": "Kort",
"NameExtraThemeSong": "Temasang",
"NameExtraThemeVideo": "Temavideo",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Ekstra"
}
@@ -1,6 +1,6 @@
{
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Artists": "Interpreten",
"Artists": "Künstler",
"AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
"Books": "Bücher",
"ChapterNameValue": "Kapitel {0}",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}"
"LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}",
"NameExtraBehindTheScenes": "Behind The Scenes",
"NameExtraDeletedScene": "Entfernte Szene",
"NameExtraInterview": "Interview",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Ausschnitt",
"NameExtraScene": "Szene",
"NameExtraShort": "Kurzfilm",
"NameExtraThemeSong": "Titellied",
"NameExtraThemeVideo": "Titelvideo",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra",
"NameExtraClip": "Clip",
"NameExtraFeaturette": "Hinter den Kulissen"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Καθαρίζει όλα τα δεδομένα χρήστη (κατάσταση παρακολούθησης, κατάσταση αγαπημένων κ.λπ.) από πολυμέσα που δεν υπάρχουν πλέον για τουλάχιστον 90 ημέρες.",
"CleanupUserDataTask": "Εργασία εκκαθάρισης δεδομένων χρήστη",
"LyricDownloadFailureFromForItem": "Αποτυχία λήψης στίχων από {0} για {1}",
"Original": "Πρωτότυπο"
"Original": "Πρωτότυπο",
"NameExtraBehindTheScenes": "Πίσω από τις Σκηνές",
"NameExtraDeletedScene": "Διεγραμμένη Σκηνή",
"NameExtraFeaturette": "Πρόσθετα βίντεο",
"NameExtraInterview": "Συνέντευξη",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Δείγμα",
"NameExtraScene": "Σκηνή",
"NameExtraShort": "Βίντεο μικρού μήκους",
"NameExtraThemeSong": "Θεματικό Τραγούδι",
"NameExtraThemeVideo": "Θεματικό Βίντεο",
"NameExtraTrailer": "τρέιλερ ταινίας",
"NameExtraUnknown": "Πρόσθετα",
"NameExtraClip": "Απόσπασμα"
}
@@ -24,8 +24,8 @@
"Music": "Music",
"MusicVideos": "Music Videos",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",
"NameSeasonNumber": "Series {0}",
"NameSeasonUnknown": "Series Unknown",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
"NotificationOptionApplicationUpdateAvailable": "Application update available",
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "User data cleanup task",
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days.",
"LyricDownloadFailureFromForItem": "Lyrics failed to download from {0} for {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Behind The Scenes",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Deleted Scene",
"NameExtraFeaturette": "Featurette",
"NameExtraInterview": "Interview",
"NameExtraSample": "Sample",
"NameExtraScene": "Scene",
"NameExtraShort": "Short",
"NameExtraThemeSong": "Theme Song",
"NameExtraThemeVideo": "Theme Video",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra",
"NameExtraNumbered": "{0} {1}"
}
@@ -97,5 +97,9 @@
"TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.",
"TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)",
"TaskAudioNormalization": "Normaligo Sonnivela",
"HearingImpaired": "Surda"
"HearingImpaired": "Surda",
"NameExtraDeletedScene": "Forigita sceno",
"NameExtraScene": "Sceno",
"NameExtraThemeSong": "Tema Kanto",
"Original": "Originala"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, estado de los favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días.",
"CleanupUserDataTask": "Tarea de limpieza de datos de usuarios",
"LyricDownloadFailureFromForItem": "No se pudo descargar la letra desde {0} para {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Detrás de cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Minidocumental",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Música de presentación",
"NameExtraThemeVideo": "Video de presentación",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra"
}
@@ -106,5 +106,20 @@
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
"CleanupUserDataTask": "Tarea de limpieza de los datos del usuario",
"CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días."
"CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días.",
"LyricDownloadFailureFromForItem": "No se pudo descargar la letra desde {0} para {1}",
"NameExtraBehindTheScenes": "Detrás de cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Minidocumental",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Música de presentación",
"NameExtraThemeVideo": "Video de presentación",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra",
"Original": "Original"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Tarea de limpieza de datos del usuario",
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}"
"LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}",
"NameExtraBehindTheScenes": "Detrás de Cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Reportaje especial",
"NameExtraInterview": "Entrevista",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Tema principal",
"NameExtraThemeVideo": "Vídeo del tema principal",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra",
"NameExtraNumbered": "{0} {1}"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Tarea de limpieza de datos de usuario",
"CleanupUserDataTaskDescription": "Limpia todos los datos de usuario (estado de visualización, favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días.",
"LyricDownloadFailureFromForItem": "No se pudo descargar las letras de {0} para {1}",
"Original": "Original"
"Original": "Original",
"NameExtraUnknown": "Extra",
"NameExtraBehindTheScenes": "Detrás de cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Minidocumental",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Música de presentación",
"NameExtraThemeVideo": "Video de presentación",
"NameExtraTrailer": "Tráiler"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Puhasta kasutajaandmed",
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud.",
"LyricDownloadFailureFromForItem": "Laulusõnade hankimine teenusest {0} loole {1} nurjus",
"Original": "Algne"
"Original": "Algne",
"NameExtraBehindTheScenes": "Kulisside taga",
"NameExtraDeletedScene": "Väljajäetud stseen",
"NameExtraFeaturette": "Lisalõik",
"NameExtraInterview": "Intervjuu",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Näidis",
"NameExtraScene": "Stseen",
"NameExtraShort": "Lühifilm",
"NameExtraThemeSong": "Tunnusmeloodia",
"NameExtraThemeVideo": "Tunnusvideo",
"NameExtraTrailer": "Treiler",
"NameExtraUnknown": "Lisamaterjal",
"NameExtraClip": "Videoklipp"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.",
"CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina",
"LyricDownloadFailureFromForItem": "Ezin izan dira {1}-ren letrak deskargatu {0}-tik",
"Original": "Jatorrizkoa"
"Original": "Jatorrizkoa",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Lagina",
"NameExtraScene": "Eszena",
"NameExtraShort": "Laburra",
"NameExtraThemeSong": "Gai-abestia",
"NameExtraThemeVideo": "Gai-bideoa",
"NameExtraBehindTheScenes": "Eszenen atzean",
"NameExtraClip": "Klipa",
"NameExtraDeletedScene": "Ezabatutako eszena",
"NameExtraFeaturette": "Erreportajea",
"NameExtraInterview": "Elkarrizketa",
"NameExtraTrailer": "Trailerra",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä",
"CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään.",
"LyricDownloadFailureFromForItem": "Sanoitusten lataus kohteesta {0} kappaleelle {1} epäonnistui",
"Original": "Alkuperäinen"
"Original": "Alkuperäinen",
"NameExtraBehindTheScenes": "Kulissien Takana",
"NameExtraClip": "Klippi",
"NameExtraDeletedScene": "Poistettu Kohtaus",
"NameExtraFeaturette": "Lyhytelokuva",
"NameExtraInterview": "Haastattelu",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Näyte",
"NameExtraScene": "Kohtaus",
"NameExtraShort": "Lyhytfilmi",
"NameExtraThemeSong": "Tunnusmusiikki",
"NameExtraThemeVideo": "Tunnusvideo",
"NameExtraTrailer": "Traileri"
}
@@ -1,19 +1,19 @@
{
"Artists": "Tónlistafólk",
"Collections": "Søvn",
"Collections": "Samlingar",
"Default": "Forsett",
"External": "Ytri",
"Genres": "Greinar",
"AppDeviceValues": "App: {0}, Eind: {1}",
"Books": "Bøkur",
"ChapterNameValue": "Kapittul {0}",
"Favorites": "Yndis",
"Folders": "Mappur",
"Favorites": "Yndislista",
"Folders": "Skjáttur",
"Forced": "Kravt",
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
"HeaderFavoriteEpisodes": "Yndispartar",
"LabelIpAddressValue": "IP-atsetur: {0}",
"AuthenticationSucceededWithUserName": "{0} varð samgildur",
"AuthenticationSucceededWithUserName": "{0} var samgildur",
"HeaderFavoriteShows": "Yndisrøðir",
"HeaderLiveTV": "Beinleiðis sjónvarp",
"HearingImpaired": "Hoyrnarveik",
@@ -55,8 +55,8 @@
"TaskUpdatePluginsDescription": "Niðurtekur og innleggur dagføringar til ískoytisforrit ið eru stillaði til at dagførast sjálvvirkandi.",
"TaskCleanTranscodeDescription": "Strikar umkotaðar fílar ið eru eldri enn 1 dag.",
"TaskOptimizeDatabase": "Albøt dátugrunn",
"NameSeasonNumber": "Sesong {0}",
"NameSeasonUnknown": "Ókend sesong",
"NameSeasonNumber": "Umfar {0}",
"NameSeasonUnknown": "Ókent umfar",
"ScheduledTaskFailedWithName": "{0} miseydnaðist",
"Undefined": "Óskilmarkað",
"TasksMaintenanceCategory": "Viðlíkahald",
@@ -68,7 +68,7 @@
"NotificationOptionServerRestartRequired": "Tørvur er á ambætaraendurbyrjan",
"TasksApplicationCategory": "Nýtsluskipan",
"NotificationOptionApplicationUpdateAvailable": "Skipanardagføring er tøk",
"NotificationOptionApplicationUpdateInstalled": "Skipanardagføring varð innløgd",
"NotificationOptionApplicationUpdateInstalled": "Skipanardagføring var innløgd",
"UserStoppedPlayingItemWithValues": "{0} er liðugur at spæla {1} á {2}",
"HomeVideos": "Heimaupptøkur",
"StartupEmbyServerIsLoading": "Jellyfin-ambætarin er undir byrjanarinnlesing. Vinaliga royn aftur um eitt bil.",
@@ -101,5 +101,25 @@
"NotificationOptionAudioPlaybackStopped": "Ljóðspæl steðgað",
"TaskExtractMediaSegments": "Leita eftir margmiðlabrotum",
"TaskExtractMediaSegmentsDescription": "Framleiður upplýsingar um brot í margmiðlum, við hjálp frá MediaSegment-virktum ískoytisforritum.",
"NotificationOptionCameraImageUploaded": "Ljósmynd uppsent"
"NotificationOptionCameraImageUploaded": "Ljósmynd uppsend",
"NameExtraShort": "Stuttfilmur",
"NameExtraThemeSong": "Eyðkennislag",
"NameExtraTrailer": "Brellbiti",
"NameExtraInterview": "Samrøða",
"NameExtraBehindTheScenes": "Aftanfyri leiktjøldini",
"NameExtraClip": "Klipp",
"NameExtraNumbered": "{0} {1}",
"NameExtraFeaturette": "Stuttur heimildarfilmur",
"TaskAudioNormalization": "Ljóðjavnan",
"TaskAudioNormalizationDescription": "Kannar fílur fyri dátum til ljóðjavnan.",
"NameExtraSample": "Sýnislutur",
"TaskRefreshTrickplayImages": "Framleið Trickplay-myndir",
"TaskRefreshTrickplayImagesDescription": "Framleiðir trickplay-myndir fyri kykmyndir í søvnunm har tað er virkt.",
"TaskMoveTrickplayImages": "Flyt Trickplay-myndagoymslustað",
"TaskMoveTrickplayImagesDescription": "Flytur verandi trickplay-fílur sambært savnsstillingunum.",
"NameExtraThemeVideo": "Eyðkenniskykmynd",
"NameExtraDeletedScene": "Úrtikin mynd",
"NameExtraScene": "Mynd",
"NameExtraUnknown": "Eykatilfar",
"Original": "Upprunalig(t/ur)"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles a échoué de {0} pour {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Dans Les Coulisses",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Scène supprimée",
"NameExtraFeaturette": "Court-métrage",
"NameExtraInterview": "Entrevue",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Échantillon",
"NameExtraScene": "Scène",
"NameExtraShort": "Court-métrage",
"NameExtraThemeSong": "Chanson thème",
"NameExtraThemeVideo": "Générique",
"NameExtraTrailer": "Bande-annonce",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles à échoué de {0} pour {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Dans Les Coulisses",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Scène supprimée",
"NameExtraFeaturette": "Court-métrage",
"NameExtraInterview": "Entrevue",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Échantillon",
"NameExtraScene": "Scène",
"NameExtraShort": "Court-métrage",
"NameExtraThemeSong": "Thème musical",
"NameExtraThemeVideo": "Générique",
"NameExtraTrailer": "Bande-annonce",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad.",
"Original": "Bunaidh",
"LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}"
"LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}",
"NameExtraBehindTheScenes": "Taobh thiar de na Radhairc",
"NameExtraClip": "Gearrthóg",
"NameExtraDeletedScene": "Radharc Scriosta",
"NameExtraFeaturette": "Mionghné",
"NameExtraInterview": "Agallamh",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Sampla",
"NameExtraScene": "Radharc",
"NameExtraShort": "Gearr",
"NameExtraThemeSong": "Amhrán Téama",
"NameExtraThemeVideo": "Físeán Téama",
"NameExtraTrailer": "Leantóir"
}
@@ -107,5 +107,19 @@
"TaskAudioNormalizationDescription": "Escanea ficheiros á procura de datos de normalización de volume.",
"CleanupUserDataTask": "Tarefa de limpeza de datos dos usuarios",
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (estado de visualización, de favorito etc.) dos medios ausentes polo menos 90 días.",
"Original": "Orixinal"
"Original": "Orixinal",
"LyricDownloadFailureFromForItem": "Non se puideron descargar as letras desde {0} para {1}",
"NameExtraBehindTheScenes": "Detrás das Cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena Eliminada",
"NameExtraFeaturette": "Reportaxe especial",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Mostra",
"NameExtraScene": "Escena",
"NameExtraShort": "Curtametraxe",
"NameExtraThemeSong": "Canción principal",
"NameExtraThemeVideo": "Vídeo da canción principal",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,12 @@
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
"CleanupUserDataTask": "משימת ניקוי מידע משתמש",
"LyricDownloadFailureFromForItem": "הורדת המילים מ-{0} עבור {1} נכשלה",
"Original": "מקור"
"Original": "מקור",
"NameExtraBehindTheScenes": "מאחורי הקלעים",
"NameExtraClip": "קליפ",
"NameExtraDeletedScene": "סצנה שנמחקה",
"NameExtraFeaturette": "סרט קצר",
"NameExtraInterview": "ריאיון",
"NameExtraSample": "דגימה",
"NameExtraScene": "סצנה"
}
@@ -106,5 +106,20 @@
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य",
"Original": "असली"
"Original": "असली",
"LyricDownloadFailureFromForItem": "{0} के लिए {1} से बोल (Lyrics) डाउनलोड करने में विफल रहा",
"NameExtraBehindTheScenes": "परदे के पीछे",
"NameExtraClip": "क्लिप",
"NameExtraDeletedScene": "हटाया गया दृश्य",
"NameExtraFeaturette": "फीचरेट",
"NameExtraInterview": "साक्षात्कार",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "नमूना",
"NameExtraScene": "दृश्य",
"NameExtraShort": "शॉर्ट",
"NameExtraThemeSong": "थीम सॉन्ग",
"NameExtraThemeVideo": "थीम वीडियो",
"NameExtraTrailer": "ट्रेलर",
"NameExtraUnknown": "अतिरिक्त",
"CleanupUserDataTaskDescription": "कम से कम 90 दिनों से अनुपस्थित मीडिया से सभी उपयोगकर्ता डेटा (देखने की स्थिति, पसंदीदा स्थिति आदि) को साफ़ करता है।"
}
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Preuzimanje tekstova pjesmi od {0} za {1} nije uspjelo"
"LyricDownloadFailureFromForItem": "Preuzimanje tekstova pjesmi od {0} za {1} nije uspjelo",
"NameExtraBehindTheScenes": "Iza kulisa",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Obrisana Scena",
"NameExtraFeaturette": "Promotivni video",
"NameExtraInterview": "Intervju",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Uzorak",
"NameExtraScene": "Scena",
"NameExtraShort": "Kratki film",
"NameExtraThemeSong": "Glavna Pjesma",
"NameExtraThemeVideo": "Tema videa",
"NameExtraTrailer": "Trailer"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.",
"CleanupUserDataTask": "Felhasználói adatok tisztítása feladat",
"Original": "Eredeti",
"LyricDownloadFailureFromForItem": "Dalszöveg letöltése {0}-tól {1}-hez sikertelen"
"LyricDownloadFailureFromForItem": "Dalszöveg letöltése {0}-tól {1}-hez sikertelen",
"NameExtraBehindTheScenes": "Színfalak mögött",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Törölt jelenet",
"NameExtraFeaturette": "Kísérő film",
"NameExtraInterview": "Interjú",
"NameExtraSample": "Minta",
"NameExtraScene": "Jelenet",
"NameExtraShort": "Rövidfilm",
"NameExtraThemeSong": "Főcímdal",
"NameExtraThemeVideo": "Főcímvideó",
"NameExtraTrailer": "Előzetes",
"NameExtraUnknown": "Extra",
"NameExtraNumbered": "{0} {1}"
}
@@ -24,5 +24,10 @@
"TaskDownloadMissingSubtitles": "Ներբեռնել պակասող ենթագրերը",
"AppDeviceValues": "Հավելված` {0}, Սարք `{1}",
"ChapterNameValue": "Գլուխ {0}",
"Collections": "Հավաքածուներ"
"Collections": "Հավաքածուներ",
"Artists": "Երաժիշտներ",
"Default": "Լռելյայն",
"Favorites": "Ընտրյալ",
"Forced": "Ստիպուած",
"Genres": "Ոճ"
}
@@ -108,5 +108,18 @@
"Original": "Upprunaleg",
"TaskExtractMediaSegmentsDescription": "Sækir myndbúta úr viðbótum þar sem MediaSegment er virkt.",
"TaskMoveTrickplayImages": "Flytja geymslustað fyrir Trickplay-myndir",
"TaskMoveTrickplayImagesDescription": "Flytur fyrirliggjandi Trickplay-skrár í samræmi við stillingar safnsins."
"TaskMoveTrickplayImagesDescription": "Flytur fyrirliggjandi Trickplay-skrár í samræmi við stillingar safnsins.",
"NameExtraBehindTheScenes": "Bak við tjöldin",
"NameExtraClip": "Brot",
"NameExtraDeletedScene": "Eydd atriði",
"NameExtraFeaturette": "Stutt heimildarmynd",
"NameExtraInterview": "Viðtal",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Sýnishorn",
"NameExtraScene": "Sena",
"NameExtraShort": "Stuttmynd",
"NameExtraThemeSong": "Þema lag",
"NameExtraThemeVideo": "Þema myndband",
"NameExtraTrailer": "Stikla",
"NameExtraUnknown": "Aukaefni"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Task di pulizia dei dati utente",
"CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni.",
"Original": "Originale",
"LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}"
"LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}",
"NameExtraBehindTheScenes": "Dietro le scene",
"NameExtraClip": "Filmato",
"NameExtraDeletedScene": "Scena eliminata",
"NameExtraInterview": "Intervista",
"NameExtraNumbered": "{0} {1}",
"NameExtraScene": "Scena",
"NameExtraSample": "Campione",
"NameExtraShort": "Corto",
"NameExtraThemeSong": "Sigla musicale",
"NameExtraTrailer": "Trailer",
"NameExtraFeaturette": "Caratteristica",
"NameExtraThemeVideo": "Video tematico",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,8 @@
"CleanupUserDataTask": "사용자 데이터 정리 작업",
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다.",
"LyricDownloadFailureFromForItem": "{1}에 대한 가사를 {0}에서 다운로드하지 못했습니다",
"Original": "원본"
"Original": "원본",
"NameExtraClip": "클립",
"NameExtraDeletedScene": "삭제된 장면",
"NameExtraInterview": "인터뷰"
}
@@ -108,5 +108,17 @@
"LyricDownloadFailureFromForItem": "Feeler beim Download vun de Songtexter vun {0} fir {1}",
"Original": "Original",
"CleanupUserDataTask": "Aufgab fir Berengege vu Benotzerdaten",
"CleanupUserDataTaskDescription": "Läscht all Benotzerdaten (Ofspillstatus, Favoritestatus, asw.) vu Medien, déi zënter mindestens 90 Deeg net méi besteeënd sinn."
"CleanupUserDataTaskDescription": "Läscht all Benotzerdaten (Ofspillstatus, Favoritestatus, asw.) vu Medien, déi zënter mindestens 90 Deeg net méi besteeënd sinn.",
"NameExtraBehindTheScenes": "Hannert de Kulissen",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Geläschte Scène",
"NameExtraFeaturette": "Featurette",
"NameExtraInterview": "Interview",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Beispill",
"NameExtraScene": "Scène",
"NameExtraShort": "Kuerzfilm",
"NameExtraThemeSong": "Theme-Lidd",
"NameExtraThemeVideo": "Theme-Video",
"NameExtraTrailer": "Bande-Annonce"
}
@@ -3,15 +3,15 @@
"Artists": "Atlikėjai",
"AuthenticationSucceededWithUserName": "{0} sėkmingai autentifikuota",
"Books": "Knygos",
"ChapterNameValue": "Scena{0}",
"Collections": "Rinkiniai",
"ChapterNameValue": "Skyrius{0}",
"Collections": "Kolekcijos",
"FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
"Favorites": "Mėgstami",
"Folders": "Katalogai",
"Folders": "Aplankai",
"Genres": "Žanrai",
"HeaderContinueWatching": "Žiūrėti toliau",
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
"HeaderFavoriteEpisodes": "Mėgstami Epizodai",
"HeaderFavoriteShows": "Mėgstamos TV Laidos",
"HeaderLiveTV": "Tiesioginė TV",
"HeaderNextUp": "Toliau",
"HomeVideos": "Namų vaizdo įrašai",
@@ -26,14 +26,14 @@
"NameInstallFailed": "{0} diegimo klaida",
"NameSeasonNumber": "Sezonas {0}",
"NameSeasonUnknown": "Sezonas neatpažintas",
"NewVersionIsAvailable": "Nauja \"Jellyfin Server\" versija yra prieinama atsisiuntimui.",
"NewVersionIsAvailable": "Nauja Jellyfin Server versija yra prieinama atsisiuntimui.",
"NotificationOptionApplicationUpdateAvailable": "Galimi programos atnaujinimai",
"NotificationOptionApplicationUpdateInstalled": "Programos atnaujinimai įdiegti",
"NotificationOptionAudioPlayback": "Garso atkūrimas pradėtas",
"NotificationOptionAudioPlaybackStopped": "Garso atkūrimas sustabdytas",
"NotificationOptionCameraImageUploaded": "Kameros vaizdai įkelti",
"NotificationOptionCameraImageUploaded": "Kameros atvaizdai įkelti",
"NotificationOptionInstallationFailed": "Diegimo klaida",
"NotificationOptionNewLibraryContent": "Naujas turinys įkeltas",
"NotificationOptionNewLibraryContent": "Pridėtas naujas turinys",
"NotificationOptionPluginError": "Įskiepio klaida",
"NotificationOptionPluginInstalled": "Įskiepis įdiegtas",
"NotificationOptionPluginUninstalled": "Įskiepis išdiegtas",
@@ -51,7 +51,7 @@
"Shows": "Laidos",
"StartupEmbyServerIsLoading": "Jellyfin Server kraunasi. Netrukus pabandykite dar kartą.",
"SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
"TvShows": "TV laidos",
"TvShows": "TV Laidos",
"UserCreatedWithName": "Buvo sukurtas {0} naudotojas",
"UserDeletedWithName": "Naudotojas {0} ištrintas",
"UserDownloadingItemWithValues": "{0} siunčiasi {1}",
@@ -59,14 +59,14 @@
"UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
"UserOnlineFromDevice": "{0} prisijungęs iš {1}",
"UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
"UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
"UserStartedPlayingItemWithValues": "{0} atkuriama {1} į {2}",
"UserStoppedPlayingItemWithValues": "{0} baigė atkūrimą {1} į {2}",
"VersionNumber": "Versija {0}",
"TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
"TaskUpdatePlugins": "Atnaujinti įskieius",
"TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
"TaskCleanTranscode": "Išvalyti perkodavimo katalogą",
"TaskCleanTranscodeDescription": "Ištrina dienos senumo transkodavimo failus.",
"TaskCleanTranscode": "Išvalyti transkodavimo katalogą",
"TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.",
"TaskRefreshLibrary": "Skenuoti medijos biblioteką",
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
@@ -76,8 +76,8 @@
"TaskRefreshPeople": "Atnaujinti žmones",
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
"TaskCleanLogs": "Išvalyti žurnalą",
"TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.",
"TaskRefreshChapterImages": "Ištraukti skyrių vaizdus",
"TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašams, kuriuose yra skyriai.",
"TaskRefreshChapterImages": "Ištraukti skyrių atvaizdus",
"TaskCleanCache": "Išvalyti talpyklą",
"TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
"TasksChannelsCategory": "Internetiniai kanalai",
@@ -87,7 +87,7 @@
"TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
"Undefined": "Neapibrėžtas",
"Forced": "Priverstinis",
"Default": "Numatytas",
"Default": "Numatytasis",
"TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.",
"TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
"TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
@@ -96,15 +96,30 @@
"External": "Išorinis",
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
"TaskRefreshTrickplayImagesDescription": "Sukuria vaizdo įrašų, esančių įgalintose bibliotekose, Trickplay peržiūras.",
"TaskAudioNormalization": "Garso normalizavimas",
"TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.",
"TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
"TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
"TaskDownloadMissingLyrics": "Atsisiųsti trūkstamus dainų tekstus",
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.",
"TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
"TaskMoveTrickplayImages": "Pakeisti Trickplay atvaizdų vietą",
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius Trickplay failus pagal bibliotekos nustatymus.",
"TaskDownloadMissingLyricsDescription": "Atsisiųsti dainų tekstus",
"CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)."
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamą būseną ir t. t.).",
"LyricDownloadFailureFromForItem": "Nepavyko atsisiųsti dainos teksto iš {0}, skirto {1}",
"NameExtraBehindTheScenes": "Užkulisiuose",
"NameExtraClip": "Klipas",
"NameExtraDeletedScene": "Ištrinta scena",
"NameExtraInterview": "Interviu",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Pavyzdys",
"NameExtraScene": "Scena",
"NameExtraThemeSong": "Teminė daina",
"NameExtraThemeVideo": "Teminis vaizdo įrašas",
"NameExtraTrailer": "Anonsas",
"NameExtraUnknown": "Papildomas",
"Original": "Originalus",
"NameExtraFeaturette": "Trumpametražis filmas",
"NameExtraShort": "Trumpas filmukas"
}
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
"CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas.",
"Original": "Oriģināls",
"LyricDownloadFailureFromForItem": "Dziesmu vārdi nevarēja tikt lejupielādēti no {0} priekš {1}"
"LyricDownloadFailureFromForItem": "Dziesmu vārdi nevarēja tikt lejupielādēti no {0} priekš {1}",
"NameExtraBehindTheScenes": "Aiz kadra",
"NameExtraClip": "Klips",
"NameExtraDeletedScene": "Izdzēsta aina",
"NameExtraFeaturette": "Īsfilma",
"NameExtraInterview": "Intervija",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Paraugs",
"NameExtraScene": "Aina",
"NameExtraShort": "Īsfilma",
"NameExtraThemeSong": "Motīvu dziesma",
"NameExtraThemeVideo": "Tēmas video",
"NameExtraTrailer": "Treileris"
}
@@ -106,5 +106,11 @@
"TaskAudioNormalization": "Normalisasi Audio",
"TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.",
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (keadaan tontonan, status kegemaran, dan sebagainya) daripada media yang tidak lagi wujud sekurang-kurangnya selama 90 hari.",
"CleanupUserDataTask": "Tugas pembersihan data pengguna"
"CleanupUserDataTask": "Tugas pembersihan data pengguna",
"LyricDownloadFailureFromForItem": "Lirik gagal dimuat turun dari {0} untuk {1}",
"NameExtraBehindTheScenes": "Di Sebalik Takbir",
"NameExtraClip": "Klip",
"NameExtraInterview": "Temu bual",
"NameExtraThemeSong": "Lagu Tema",
"NameExtraThemeVideo": "Video Tema"
}
@@ -106,5 +106,15 @@
"TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.",
"TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment.",
"CleanupUserDataTaskDescription": "Sletter all brukerdata (avspillings-status, favoritter osv.) fra innhold som har vært utilgjengelig i minst 90 dager.",
"CleanupUserDataTask": "Oppgave for opprydding av brukerdata"
"CleanupUserDataTask": "Oppgave for opprydding av brukerdata",
"NameExtraBehindTheScenes": "Bak kulissene",
"NameExtraDeletedScene": "Slettet scene",
"NameExtraFeaturette": "Presentasjonsfilm",
"NameExtraInterview": "Intervju",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Prøve",
"NameExtraScene": "Scene",
"NameExtraThemeSong": "Tema-låt",
"NameExtraThemeVideo": "Tema-video",
"NameExtraTrailer": "Trailer"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Usuwa wszystkie dane użytkownika (stan oglądanych, status ulubionych itp.) z mediów, które nie są dostępne od co najmniej 90 dni.",
"CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika",
"Original": "Oryginalny",
"LyricDownloadFailureFromForItem": "Błąd podczas pobierania tekstu piosenki z {0} dla {1}"
"LyricDownloadFailureFromForItem": "Błąd podczas pobierania tekstu piosenki z {0} dla {1}",
"NameExtraBehindTheScenes": "Za kulisami",
"NameExtraClip": "Urywek",
"NameExtraDeletedScene": "Usunięta scena",
"NameExtraFeaturette": "Film średniometrażowy",
"NameExtraInterview": "Wywiad",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Fragment",
"NameExtraScene": "Scena",
"NameExtraShort": "Film krótkometrażowy",
"NameExtraThemeSong": "Czołówka",
"NameExtraThemeVideo": "Wideo wprowadzające",
"NameExtraTrailer": "Zwiastun",
"NameExtraUnknown": "Dodatek"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Tarefa de limpeza de dados do usuário",
"CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias.",
"LyricDownloadFailureFromForItem": "Download das Letras falharam em {0} para o item {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Nos Bastidores",
"NameExtraClip": "Clipe",
"NameExtraDeletedScene": "cena Extra",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Trecho",
"NameExtraScene": "Cena",
"NameExtraShort": "Curta-metragem",
"NameExtraThemeSong": "Música Tema",
"NameExtraThemeVideo": "Vídeo de Abertura",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra",
"NameExtraFeaturette": "Nos Bastidores",
"NameExtraInterview": "Entrevista"
}
@@ -5,108 +5,121 @@
"Books": "Livros",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Coleções",
"FailedLoginAttemptWithUserName": "Tentativa de login falhada a partir de {0}",
"FailedLoginAttemptWithUserName": "Tentativa falhada de início de sessão do utilizador {0}",
"Favorites": "Favoritos",
"Folders": "Pastas",
"Genres": "Géneros",
"HeaderContinueWatching": "Continuar a ver",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
"HeaderLiveTV": "TV em Direto",
"HeaderNextUp": "A Seguir",
"HomeVideos": "Vídeos Caseiros",
"HeaderFavoriteEpisodes": "Episódios favoritos",
"HeaderFavoriteShows": "Séries favoritas",
"HeaderLiveTV": "TV em direto",
"HeaderNextUp": "A seguir",
"HomeVideos": "Vídeos caseiros",
"Inherit": "Herdar",
"LabelIpAddressValue": "Endereço IP: {0}",
"LabelRunningTimeValue": "Duração: {0}",
"Latest": "Mais Recente",
"MixedContent": "Conteúdo Misto",
"Latest": "Mais recente",
"MixedContent": "Conteúdo misto",
"Movies": "Filmes",
"Music": "Música",
"MusicVideos": "Videoclipes",
"NameInstallFailed": "{0} falha na instalação",
"NameInstallFailed": "Falha na instalação de {0}",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconhecida",
"NameSeasonUnknown": "Temporada desconhecida",
"NewVersionIsAvailable": "Está disponível para transferência uma nova versão do servidor Jellyfin.",
"NotificationOptionApplicationUpdateAvailable": "Atualização de aplicação disponível",
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicação instalada",
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
"NotificationOptionAudioPlayback": "Reprodução de áudio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reprodução de áudio interrompida",
"NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
"NotificationOptionInstallationFailed": "Falha na instalação",
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
"NotificationOptionPluginError": "Falha na extensão",
"NotificationOptionPluginInstalled": "Extensão instalada",
"NotificationOptionPluginUninstalled": "Extensão desinstalada",
"NotificationOptionPluginUpdateInstalled": "Extensão atualizada",
"NotificationOptionPluginError": "Falha no plugin",
"NotificationOptionPluginInstalled": "Plugin instalado",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
"NotificationOptionPluginUpdateInstalled": "Atualização de plugin instalada",
"NotificationOptionServerRestartRequired": "Necessário reiniciar o servidor",
"NotificationOptionTaskFailed": "Falha em tarefa agendada",
"NotificationOptionUserLockedOut": "Utilizador bloqueado",
"NotificationOptionVideoPlayback": "Reprodução do vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reprodução do vídeo parada",
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo interrompida",
"Photos": "Fotografias",
"PluginInstalledWithName": "{0} foi instalado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginUpdatedWithName": "{0} foi atualizado",
"ScheduledTaskFailedWithName": "{0} falhou",
"Shows": "Séries",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente mais tarde.",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}",
"TvShows": "Séries",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tenta novamente dentro de instantes.",
"SubtitleDownloadFailureFromForItem": "Falha ao transferir legendas de {0} para {1}",
"TvShows": "Séries de TV",
"UserCreatedWithName": "Utilizador {0} criado",
"UserDeletedWithName": "Utilizador {0} apagado",
"UserDeletedWithName": "Utilizador {0} eliminado",
"UserDownloadingItemWithValues": "{0} está a transferir {1}",
"UserLockedOutWithName": "Utilizador {0} bloqueado",
"UserOfflineFromDevice": "{0} desligou-se a partir de {1}",
"UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
"UserOfflineFromDevice": "{0} desligou-se de {1}",
"UserOnlineFromDevice": "{0} está online em {1}",
"UserPasswordChangedWithName": "Palavra-passe alterada para o utilizador {0}",
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
"VersionNumber": "Versão {0}",
"TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas em falta baseado na configuração de metadados.",
"TaskDownloadMissingSubtitlesDescription": "Procura na Internet legendas em falta com base na configuração dos metadados.",
"TaskDownloadMissingSubtitles": "Transferir legendas em falta",
"TaskRefreshChannelsDescription": "Atualizar informação sobre canais da Internet.",
"TaskRefreshChannels": "Atualizar Canais",
"TaskCleanTranscodeDescription": "Apagar ficheiros de transcode com mais de um dia.",
"TaskCleanTranscode": "Limpar a Diretoria de Transcode",
"TaskUpdatePluginsDescription": "Faz o download e instala updates para os plugins que estão configurados para atualizar automaticamente.",
"TaskUpdatePlugins": "Atualizar Plugins",
"TaskRefreshPeopleDescription": "Atualizar metadados para elenco e equipa técnica da tua mediateca.",
"TaskRefreshPeople": "Atualizar Pessoas",
"TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.",
"TaskCleanLogs": "Limpar a Diretoria de Logs",
"TaskRefreshLibraryDescription": "Analisar a mediateca para novos ficheiros e atualizar os metadados.",
"TaskRefreshLibrary": "Analisar mediateca",
"TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.",
"TaskRefreshChapterImages": "Extrair Imagens dos Capítulos",
"TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.",
"TaskCleanCache": "Limpar Cache",
"TaskRefreshChannelsDescription": "Atualiza as informações dos canais da Internet.",
"TaskRefreshChannels": "Atualizar canais",
"TaskCleanTranscodeDescription": "Elimina ficheiros de transcodificação com mais de um dia.",
"TaskCleanTranscode": "Limpar pasta de transcodificação",
"TaskUpdatePluginsDescription": "Transfere e instala atualizações dos plugins configurados para atualização automática.",
"TaskUpdatePlugins": "Atualizar plugins",
"TaskRefreshPeopleDescription": "Atualiza os metadados de atores e realizadores na tua biblioteca multimédia.",
"TaskRefreshPeople": "Atualizar pessoas",
"TaskCleanLogsDescription": "Elimina ficheiros de registo com mais de {0} dias.",
"TaskCleanLogs": "Limpar pasta de registos",
"TaskRefreshLibraryDescription": "Analisa a biblioteca multimédia à procura de novos ficheiros e atualiza os metadados.",
"TaskRefreshLibrary": "Analisar biblioteca multimédia",
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
"TaskRefreshChapterImages": "Extrair imagens dos capítulos",
"TaskCleanCacheDescription": "Elimina ficheiros de cache que já não são necessários ao sistema.",
"TaskCleanCache": "Limpar pasta de cache",
"TasksChannelsCategory": "Canais da Internet",
"TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Mediateca",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskCleanActivityLogDescription": "Apaga as entradas do registo de atividade anteriores à data configurada.",
"TaskCleanActivityLogDescription": "Elimina as entradas do registo de atividade mais antigas do que o período configurado.",
"TaskCleanActivityLog": "Limpar registo de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
"Default": "Predefinição",
"TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
"TaskOptimizeDatabaseDescription": "Compacta a base de dados e liberta espaço não utilizado. A execução desta tarefa depois de analisar a biblioteca ou de outras alterações que modifiquem a base de dados pode melhorar o desempenho.",
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
"TaskKeyframeExtractorDescription": "Extrai fotogramas-chave de ficheiros de vídeo para criar playlists HLS mais precisas. Esta tarefa pode demorar algum tempo.",
"TaskKeyframeExtractor": "Extrator de fotogramas-chave",
"External": "Externo",
"HearingImpaired": "Surdo",
"HearingImpaired": "Deficiência auditiva",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
"TaskAudioNormalization": "Normalização de áudio",
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
"TaskDownloadMissingLyrics": "Transferir letra em falta",
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
"TaskExtractMediaSegments": "Analisar segmentos multimédia",
"TaskDownloadMissingLyrics": "Transferir letras em falta",
"TaskMoveTrickplayImages": "Migrar localização das imagens de trickplay",
"TaskDownloadMissingLyricsDescription": "Transfere letras de músicas",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos multimédia de plugins com MediaSegment ativado.",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de trickplay existentes de acordo com as definições da biblioteca.",
"CleanupUserDataTaskDescription": "Remove todos os dados de utilizador (estado de reprodução, estado de favorito, etc.) de conteúdos multimédia que já não estejam presentes há pelo menos 90 dias.",
"CleanupUserDataTask": "Limpeza de dados de utilizador",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
"LyricDownloadFailureFromForItem": "Falha ao transferir letras de {0} para {1}",
"NameExtraDeletedScene": "Cena eliminada",
"NameExtraInterview": "Entrevista",
"NameExtraUnknown": "Extra",
"NameExtraBehindTheScenes": "Bastidores",
"NameExtraSample": "Amostra",
"NameExtraScene": "Cena",
"NameExtraClip": "Excerto",
"NameExtraFeaturette": "Minidocumentário",
"NameExtraThemeSong": "Tema musical",
"NameExtraThemeVideo": "Vídeo temático",
"NameExtraShort": "Curta-metragem",
"NameExtraNumbered": "{0} {1}",
"NameExtraTrailer": "Trailer"
}
@@ -1,112 +1,125 @@
{
"HeaderLiveTV": "TV Em Direto",
"HeaderLiveTV": "TV em direto",
"Collections": "Coleções",
"Books": "Livros",
"Artists": "Artistas",
"HeaderNextUp": "A Seguir",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
"HeaderNextUp": "A seguir",
"HeaderFavoriteEpisodes": "Episódios favoritos",
"HeaderFavoriteShows": "Séries favoritas",
"HeaderContinueWatching": "Continuar a ver",
"Genres": "Géneros",
"Folders": "Pastas",
"Favorites": "Favoritos",
"UserDownloadingItemWithValues": "{0} está sendo baixado {1}",
"UserDownloadingItemWithValues": "{0} está a transferir {1}",
"VersionNumber": "Versão {0}",
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
"UserStartedPlayingItemWithValues": "{0} está reproduzindo {1} em {2}",
"UserPasswordChangedWithName": "A senha do usuário {0} foi alterada",
"UserOnlineFromDevice": "{0} está online a partir de {1}",
"UserOfflineFromDevice": "{0} desconectou-se a partir de {1}",
"UserLockedOutWithName": "O usuário {0} foi bloqueado",
"UserDeletedWithName": "O usuário {0} foi removido",
"UserCreatedWithName": "O usuário {0} foi criado",
"TvShows": "Séries",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está iniciando. Tente novamente dentro de momentos.",
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
"UserPasswordChangedWithName": "Palavra-passe alterada para o utilizador {0}",
"UserOnlineFromDevice": "{0} está online em {1}",
"UserOfflineFromDevice": "{0} desligou-se de {1}",
"UserLockedOutWithName": "Utilizador {0} bloqueado",
"UserDeletedWithName": "Utilizador {0} eliminado",
"UserCreatedWithName": "Utilizador {0} criado",
"TvShows": "Séries de TV",
"SubtitleDownloadFailureFromForItem": "Falha ao transferir legendas de {0} para {1}",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tenta novamente dentro de instantes.",
"ScheduledTaskFailedWithName": "{0} falhou",
"PluginUpdatedWithName": "{0} foi atualizado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginInstalledWithName": "{0} foi instalado",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo interrompida",
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
"NotificationOptionUserLockedOut": "Usuário bloqueado",
"NotificationOptionTaskFailed": "Falha na tarefa agendada",
"NotificationOptionServerRestartRequired": "É necessário reiniciar o servidor",
"NotificationOptionPluginUpdateInstalled": "Plugin atualizado",
"NotificationOptionUserLockedOut": "Utilizador bloqueado",
"NotificationOptionTaskFailed": "Falha em tarefa agendada",
"NotificationOptionServerRestartRequired": "Necessário reiniciar o servidor",
"NotificationOptionPluginUpdateInstalled": "Atualização de plugin instalada",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
"NotificationOptionPluginInstalled": "Plugin instalado",
"NotificationOptionPluginError": "Falha no plugin",
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
"NotificationOptionInstallationFailed": "Falha de instalação",
"NotificationOptionCameraImageUploaded": "Imagem de câmera enviada",
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
"NotificationOptionApplicationUpdateInstalled": "A atualização do aplicativo foi instalada",
"NotificationOptionApplicationUpdateAvailable": "Uma atualização do aplicativo está disponível",
"NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.",
"NameSeasonUnknown": "Temporada Desconhecida",
"NotificationOptionInstallationFailed": "Falha na instalação",
"NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
"NotificationOptionAudioPlaybackStopped": "Reprodução de áudio interrompida",
"NotificationOptionAudioPlayback": "Reprodução de áudio iniciada",
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicação instalada",
"NotificationOptionApplicationUpdateAvailable": "Atualização de aplicação disponível",
"NewVersionIsAvailable": "Está disponível para transferência uma nova versão do servidor Jellyfin.",
"NameSeasonUnknown": "Temporada desconhecida",
"NameSeasonNumber": "Temporada {0}",
"NameInstallFailed": "Falha na instalação de {0}",
"MusicVideos": "Videoclipes",
"Music": "Música",
"MixedContent": "Conteúdo diverso",
"Latest": "Mais Recente",
"MixedContent": "Conteúdo misto",
"Latest": "Mais recente",
"LabelRunningTimeValue": "Duração: {0}",
"LabelIpAddressValue": "Endereço de IP: {0}",
"LabelIpAddressValue": "Endereço IP: {0}",
"Inherit": "Herdar",
"HomeVideos": "Vídeos Caseiros",
"HomeVideos": "Vídeos caseiros",
"Shows": "Séries",
"Photos": "Fotografias",
"Movies": "Filmes",
"FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}",
"FailedLoginAttemptWithUserName": "Tentativa falhada de início de sessão do utilizador {0}",
"ChapterNameValue": "Capítulo {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
"TaskCleanCache": "Limpar pasta de cache",
"TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Mediateca",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskRefreshChannels": "Atualizar Canais",
"TaskUpdatePlugins": "Atualizar Plugins",
"TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
"TaskCleanLogs": "Limpar diretório de logs",
"TaskRefreshLibrary": "Analisar mediateca",
"TaskRefreshChannels": "Atualizar canais",
"TaskUpdatePlugins": "Atualizar plugins",
"TaskCleanLogsDescription": "Elimina ficheiros de registo com mais de {0} dias.",
"TaskCleanLogs": "Limpar pasta de registos",
"TaskRefreshLibrary": "Analisar biblioteca multimédia",
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
"TasksChannelsCategory": "Canais de Internet",
"TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
"TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
"TaskCleanCacheDescription": "Elimina ficheiros de cache que já não são necessários ao sistema.",
"TasksChannelsCategory": "Canais da Internet",
"TaskRefreshChapterImages": "Extrair imagens dos capítulos",
"TaskDownloadMissingSubtitlesDescription": "Procura na Internet legendas em falta com base na configuração dos metadados.",
"TaskDownloadMissingSubtitles": "Transferir legendas em falta",
"TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
"TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
"TaskCleanTranscode": "Limpar o diretório de Transcode",
"TaskUpdatePluginsDescription": "Baixa e instala as atualizações para plug-ins configurados para atualização automática.",
"TaskRefreshPeopleDescription": "Atualizar metadados para elenco e equipa técnica da tua mediateca.",
"TaskRefreshChannelsDescription": "Atualiza as informações dos canais da Internet.",
"TaskCleanTranscodeDescription": "Elimina ficheiros de transcodificação com mais de um dia.",
"TaskCleanTranscode": "Limpar pasta de transcodificação",
"TaskUpdatePluginsDescription": "Transfere e instala atualizações dos plugins configurados para atualização automática.",
"TaskRefreshPeopleDescription": "Atualiza os metadados de atores e realizadores na tua biblioteca multimédia.",
"TaskRefreshPeople": "Atualizar pessoas",
"TaskRefreshLibraryDescription": "Analisar a mediateca para novos ficheiros e atualizar os metadados.",
"TaskCleanActivityLog": "Limpar registro de atividade",
"TaskRefreshLibraryDescription": "Analisa a biblioteca multimédia à procura de novos ficheiros e atualiza os metadados.",
"TaskCleanActivityLog": "Limpar registo de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
"Default": "Predefinição",
"TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.",
"TaskCleanActivityLogDescription": "Elimina as entradas do registo de atividade mais antigas do que o período configurado.",
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
"TaskOptimizeDatabaseDescription": "Compacta a base de dados e liberta espaço não utilizado. A execução desta tarefa depois de analisar a biblioteca ou de outras alterações que modifiquem a base de dados pode melhorar o desempenho.",
"External": "Externo",
"HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"HearingImpaired": "Deficiência auditiva",
"TaskKeyframeExtractor": "Extrator de fotogramas-chave",
"TaskKeyframeExtractorDescription": "Extrai fotogramas-chave de ficheiros de vídeo para criar playlists HLS mais precisas. Esta tarefa pode demorar algum tempo.",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
"TaskAudioNormalization": "Normalização de áudio",
"TaskDownloadMissingLyrics": "Transferir letra em falta",
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
"CleanupUserDataTask": "Task de limpeza de dados do usuário",
"CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de dias que não estão presentes há pelo menos 90 dias.",
"TaskDownloadMissingLyrics": "Transferir letras em falta",
"TaskDownloadMissingLyricsDescription": "Transfere letras de músicas",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de trickplay existentes de acordo com as definições da biblioteca.",
"TaskExtractMediaSegments": "Analisar segmentos multimédia",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos multimédia de plugins com MediaSegment ativado.",
"TaskMoveTrickplayImages": "Migrar localização das imagens de trickplay",
"CleanupUserDataTask": "Limpeza de dados de utilizador",
"CleanupUserDataTaskDescription": "Remove todos os dados de utilizador (estado de reprodução, estado de favorito, etc.) de conteúdos multimédia que não estejam presentes há pelo menos 90 dias.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
"LyricDownloadFailureFromForItem": "Falha ao transferir letras de {0} para {1}",
"NameExtraBehindTheScenes": "Bastidores",
"NameExtraClip": "Excerto",
"NameExtraDeletedScene": "Cena eliminada",
"NameExtraFeaturette": "Minidocumentário",
"NameExtraInterview": "Entrevista",
"NameExtraSample": "Amostra",
"NameExtraShort": "Curta-metragem",
"NameExtraThemeSong": "Tema musical",
"NameExtraThemeVideo": "Vídeo temático",
"NameExtraScene": "Cena",
"NameExtraUnknown": "Extra",
"NameExtraTrailer": "Trailer",
"NameExtraNumbered": "{0} {1}"
}
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului",
"CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile.",
"LyricDownloadFailureFromForItem": "Versurile nu au putut fi descărcate din {0} pentru {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "În culise",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Scenă ștearsă",
"NameExtraFeaturette": "Material bonus",
"NameExtraInterview": "Interviu",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Monstră",
"NameExtraScene": "Scenă",
"NameExtraShort": "Scurt",
"NameExtraThemeSong": "Audio de Fundal",
"NameExtraThemeVideo": "Video de Fundal",
"NameExtraTrailer": "Trailer"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Задача очистки пользовательских данных",
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней.",
"Original": "Оригинальный",
"LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}"
"LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}",
"NameExtraBehindTheScenes": "За кадром",
"NameExtraClip": "Отрывок",
"NameExtraDeletedScene": "Удалённая сцена",
"NameExtraFeaturette": "Короткометражка",
"NameExtraInterview": "Интервью",
"NameExtraSample": "Образец",
"NameExtraScene": "Сцена",
"NameExtraThemeSong": "Заглавная песня",
"NameExtraThemeVideo": "Заглавное видео",
"NameExtraTrailer": "Трейлер",
"NameExtraUnknown": "Дополнительный материал",
"NameExtraNumbered": "{0} {1}",
"NameExtraShort": "Короткометражка"
}
@@ -91,7 +91,7 @@
"Default": "Predvolené",
"TaskOptimizeDatabaseDescription": "Zmenší databázu a odstráni prázdne miesto. Spustenie tejto úlohy po skenovaní knižnice alebo po iných zmenách zahŕňajúcich úpravy databáze môže zlepšiť výkon.",
"TaskOptimizeDatabase": "Optimalizovať databázu",
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z videosúborov na vytvorenie presnejších HLS zoznamov. Táto úloha môže trvať dlhší čas.",
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
"External": "Externé",
"HearingImpaired": "Sluchovo postihnutí",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Prečistiť používateľské dáta",
"CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní.",
"LyricDownloadFailureFromForItem": "Text piesne sa nepodarilo stiahnuť z {0} pre {1}",
"Original": "Originál"
"Original": "Originál",
"NameExtraBehindTheScenes": "Zo zákulisia",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Vystrihnutá scéna",
"NameExtraFeaturette": "Bonus",
"NameExtraInterview": "Rozhovor",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Ukážka",
"NameExtraScene": "Scéna",
"NameExtraShort": "Krátky film",
"NameExtraThemeSong": "Úvodná pieseň",
"NameExtraThemeVideo": "Úvodné video",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Čiščenje uporabniških podatkov",
"CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo.",
"LyricDownloadFailureFromForItem": "Besedila ni bilo mogoče prenesti iz {0} za {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "V zakulisju",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Izbrisan prizor",
"NameExtraFeaturette": "Kratek dokumentarec o izdelavi filma",
"NameExtraInterview": "Intervju",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Vzorec",
"NameExtraScene": "Prizor",
"NameExtraShort": "Kratki film",
"NameExtraThemeSong": "Tematska Pesem",
"NameExtraThemeVideo": "Tematski Video",
"NameExtraTrailer": "Napovednik"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Pastron të gjitha të dhënat e përdorueseve (gjendja e shikimit, statusi i të preferuarave etj.) nga mediat që nuk janë më të pranishme për të paktën 90 ditë.",
"CleanupUserDataTask": "Veprim për pastrimin të dhënave të përdorueseve",
"LyricDownloadFailureFromForItem": "Teksti i këngës nuk arriti të shkarkohej nga {0} për {1}",
"Original": "Origjinal"
"Original": "Origjinal",
"NameExtraBehindTheScenes": "Pamje nga prapaskenat",
"NameExtraClip": "Pjesë",
"NameExtraDeletedScene": "Skenë e fshirë",
"NameExtraFeaturette": "Film i shkurtër",
"NameExtraInterview": "Intervistë",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Shembull",
"NameExtraScene": "Skenë",
"NameExtraShort": "Film i shkurtër",
"NameExtraThemeSong": "Kënga e temës",
"NameExtraThemeVideo": "Videoja e temës",
"NameExtraTrailer": "Parapamje",
"NameExtraUnknown": "Shtesë"
}
@@ -108,5 +108,18 @@
"TaskMoveTrickplayImages": "Промени локацију сличица за визуелно премотавање",
"TaskDownloadMissingLyricsDescription": "Преузми стихове песама",
"LyricDownloadFailureFromForItem": "Није успело преузимање стихова са {0} за {1}",
"Original": "Изворно"
"Original": "Изворно",
"NameExtraBehindTheScenes": "Иза кулиса",
"NameExtraClip": "Исечак",
"NameExtraDeletedScene": "Обрисана сцена",
"NameExtraFeaturette": "Кратки прилог",
"NameExtraInterview": "Интервју",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Узорак",
"NameExtraScene": "Сцена",
"NameExtraShort": "Кратки филм",
"NameExtraThemeSong": "Музичка тема",
"NameExtraThemeVideo": "Спот музичке теме",
"NameExtraTrailer": "Реклама",
"NameExtraUnknown": "Додатак"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.",
"CleanupUserDataTask": "Uppgift för rensning av användardata",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Misslyckades att ladda ner låttexter från {0} för {1}"
"LyricDownloadFailureFromForItem": "Misslyckades att ladda ner låttexter från {0} för {1}",
"NameExtraBehindTheScenes": "Bakom kulisserna",
"NameExtraDeletedScene": "Borttagen scen",
"NameExtraInterview": "Intervju",
"NameExtraNumbered": "{0} {1}",
"NameExtraScene": "Scen",
"NameExtraShort": "Kortfilm",
"NameExtraThemeSong": "Signaturmelodi",
"NameExtraTrailer": "Trailer",
"NameExtraClip": "Klipp",
"NameExtraFeaturette": "Kortfilm",
"NameExtraSample": "Prov",
"NameExtraThemeVideo": "Signaturvideo",
"NameExtraUnknown": "Extra"
}
@@ -106,5 +106,20 @@
"TaskExtractMediaSegments": "மீடியா பிரிவு ஸ்கேன்",
"TaskExtractMediaSegmentsDescription": "மீடியாசெக்மென்ட் இயக்கப்பட்ட செருகுநிரல்களிலிருந்து மீடியா பிரிவுகளைப் பிரித்தெடுக்கிறது அல்லது பெறுகிறது.",
"CleanupUserDataTaskDescription": "குறைந்தது 90 நாட்களுக்கு இல்லாத மீடியாவிலிருந்து அனைத்து பயனர் தரவையும் (கண்காணிப்பு நிலை, பிடித்த நிலை போன்றவை) சுத்தம் செய்கிறது.",
"CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி"
"CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி",
"LyricDownloadFailureFromForItem": "{0} இலிருந்து {1} க்கு பாடல் வரிகளைப் பதிவிறக்க முடியவில்லை",
"NameExtraBehindTheScenes": "திரைக்குப் பின்னால்",
"NameExtraClip": "துண்டுக்காட்சி",
"NameExtraDeletedScene": "நீக்கப்பட்ட காட்சிகள்",
"NameExtraFeaturette": "திரைப்பின்னணித் தொகுப்பு",
"NameExtraInterview": "நேர்காணல்",
"NameExtraSample": "மாதிரி",
"NameExtraScene": "காட்சி",
"NameExtraShort": "குறுகிய",
"NameExtraThemeSong": "மையப் பாடல்",
"NameExtraThemeVideo": "மையக் காணொளி",
"NameExtraTrailer": "முன்னோட்டம்",
"NameExtraUnknown": "கூடுதல்",
"Original": "அசல்",
"NameExtraNumbered": "{0} {1}"
}
@@ -22,20 +22,20 @@
"NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
"NameSeasonUnknown": "ไม่ทราบซีซัน",
"NameSeasonNumber": "ซีซัน {0}",
"NameInstallFailed": "การติดตั้ง {0} ล้มเหลว",
"NameInstallFailed": "ติดตั้ง {0} ไม่สำเร็จ",
"MusicVideos": "มิวสิควิดีโอ",
"Music": "ดนตรี",
"Music": "เพลง",
"Movies": "ภาพยนตร์",
"MixedContent": "เนื้อหาผสม",
"Latest": "ล่าสุด",
"LabelRunningTimeValue": "ผ่านไปแล้ว: {0}",
"LabelIpAddressValue": "ที่อยู่ IP: {0}",
"Inherit": "สืบทอด",
"HomeVideos": "โฮมวิดีโอ",
"HeaderNextUp": "ถัดไป",
"HeaderLiveTV": "ทีวีสด",
"HeaderFavoriteShows": "รายการที่ชื่นชอบ",
"HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ",
"MixedContent": "เนื้อหาหลากหลายประเภท",
"Latest": "มาใหม่ล่าสุด",
"LabelRunningTimeValue": "ความยาว: {0}",
"LabelIpAddressValue": "หมายเลข IP: {0}",
"Inherit": "ใช้ค่าเริ่มต้น",
"HomeVideos": "วิดีโอส่วนตัว",
"HeaderNextUp": "รายการถัดไป",
"HeaderLiveTV": "ทีวีถ่ายทอดสด",
"HeaderFavoriteShows": "รายการที่ชอบ",
"HeaderFavoriteEpisodes": "ตอนที่ชอบ",
"HeaderContinueWatching": "ดูต่อ",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
@@ -107,6 +107,19 @@
"TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay",
"CleanupUserDataTask": "ส่วนงานล้างข้อมูลผู้ใช้",
"CleanupUserDataTaskDescription": "ล้างข้อมูลผู้ใช้ทั้งหมด (สถานะการรับชม สถานะรายการโปรด ฯลฯ) จากสื่อที่ไม่ได้ใช้งานแล้วอย่างน้อย 90 วัน",
"LyricDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดเนื้อเพลงจาก {0} สำหรับ {1}",
"Original": "ต้นฉบับ"
"LyricDownloadFailureFromForItem": "ดาวน์โหลดเนื้อเพลงจาก {0} สำหรับ {1} ไม่สำเร็จ",
"Original": "ต้นฉบับ",
"NameExtraBehindTheScenes": "เบื้องหลังการถ่ายทำ",
"NameExtraClip": "คลิปวิดีโอ",
"NameExtraDeletedScene": "ฉากที่ถูกตัดออก",
"NameExtraFeaturette": "คลิปสั้นพิเศษ",
"NameExtraInterview": "บทสัมภาษณ์",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "ตัวอย่าง",
"NameExtraScene": "ฉาก",
"NameExtraShort": "ภาพยนตร์สั้น",
"NameExtraThemeSong": "เพลงประกอบ",
"NameExtraThemeVideo": "วิดีโอธีม",
"NameExtraTrailer": "ตัวอย่างภาพยนตร์",
"NameExtraUnknown": "เนื้อหาพิเศษ"
}
@@ -1,5 +1,5 @@
{
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
"AppDeviceValues": "Uygulama: {0}, Cihaz: {1}",
"Artists": "Sanatçılar",
"AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı",
"Books": "Kitaplar",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Kullanıcı verisi temizleme görevi",
"CleanupUserDataTaskDescription": "En az 90 gün boyunca artık mevcut olmayan medyadaki tüm kullanıcı verilerini (İzleme durumu, favori durumu vb.) temizler.",
"LyricDownloadFailureFromForItem": "{1} şarkı sözleri {0} adresinden indirilemedi",
"Original": "Orijinal"
"Original": "Orijinal",
"NameExtraBehindTheScenes": "Kamera Arkası",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Silinmiş Sahne",
"NameExtraFeaturette": "Kısa film",
"NameExtraInterview": "Röportaj",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Örnek",
"NameExtraScene": "Sahne",
"NameExtraShort": "Kısa",
"NameExtraThemeSong": "Tanıtım Müziği",
"NameExtraThemeVideo": "Tanıtım Videosu",
"NameExtraTrailer": "Fragman",
"NameExtraUnknown": "Fazladan"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Завдання очищення даних користувача",
"CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому.",
"Original": "Оригінал",
"LyricDownloadFailureFromForItem": "Не вдалося завантажити текст пісні з {0} для {1}"
"LyricDownloadFailureFromForItem": "Не вдалося завантажити текст пісні з {0} для {1}",
"NameExtraBehindTheScenes": "За лаштунками",
"NameExtraClip": "Кліп",
"NameExtraDeletedScene": "Видалена сцена",
"NameExtraFeaturette": "Фічуретка",
"NameExtraInterview": "Інтерв’ю",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Приклад",
"NameExtraScene": "Сцена",
"NameExtraShort": "Коротко",
"NameExtraThemeSong": "Тематична пісня",
"NameExtraThemeVideo": "Тематичне вiдео",
"NameExtraTrailer": "Трейлер",
"NameExtraUnknown": "Додатково"
}
@@ -32,7 +32,7 @@
"TasksLibraryCategory": "Thư Viện",
"TasksMaintenanceCategory": "Bảo Trì",
"VersionNumber": "Phiên Bản {0}",
"UserStoppedPlayingItemWithValues": "{0} đã kết thúc phát {1} trên {2}",
"UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
"UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
"UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
"UserOnlineFromDevice": "{0} trực tuyến từ {1}",
@@ -79,7 +79,7 @@
"HeaderNextUp": "Tiếp Theo",
"HeaderFavoriteShows": "Chương Trình Yêu Thích",
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập không thành công từ {0}",
"FailedLoginAttemptWithUserName": "Cố gắng đăng nhập thất bại từ {0}",
"ChapterNameValue": "Phân Cảnh {0}",
"Books": "Sách",
"AuthenticationSucceededWithUserName": "{0} xác thực thành công",
@@ -95,18 +95,31 @@
"TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài.",
"External": "Bên ngoài",
"HearingImpaired": "Khiếm Thính",
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
"TaskRefreshTrickplayImages": "Tạo Ảnh Tua Nhanh (Trickplay)",
"TaskRefreshTrickplayImagesDescription": "Tạo ảnh tua nhanh (trịckplay) xem thử cho các video trong các thư viện được kích hoạt.",
"TaskAudioNormalization": "Chuẩn Hóa Âm Thanh",
"TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh.",
"TaskDownloadMissingLyricsDescription": "Tải xuống lời cho bài hát",
"TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu",
"TaskExtractMediaSegmentsDescription": "Trích xuất hoặc lấy các phân đoạn phương tiện từ các plugin hỗ trợ MediaSegment.",
"TaskMoveTrickplayImages": "Di chuyển vtnh nh Trickplay",
"TaskMoveTrickplayImagesDescription": "Di chuyển các tp tin trickplay hiện có theo cài đặt thư viện.",
"TaskMoveTrickplayImages": "Di Chuyển VTnh Tua Nhanh (Trickplay)",
"TaskMoveTrickplayImagesDescription": "Di chuyển các tp ảnh tua nhanh (trickplay) hiện có theo cài đặt thư viện.",
"TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện",
"CleanupUserDataTask": "Tác vụ dọn dẹp dữ liệu người dùng",
"CleanupUserDataTaskDescription": "Làm sạch tất cả dữ liệu người dùng (trạng thái xem, trạng thái yêu thích, v.v.) từ phương tiện không còn có mặt trong ít nhất 90 ngày.",
"Original": "Gốc",
"LyricDownloadFailureFromForItem": "Lời bài hát không tải xuống được từ {0} cho {1}"
"LyricDownloadFailureFromForItem": "Lời bài hát không tải xuống được từ {0} cho {1}",
"NameExtraBehindTheScenes": "Hậu Trường",
"NameExtraDeletedScene": "Cảnh Bị Xóa",
"NameExtraInterview": "Phỏng vấn",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Mẫu",
"NameExtraScene": "Cảnh",
"NameExtraShort": "Ngắn",
"NameExtraThemeSong": "Bài Hát Chủ Đề",
"NameExtraThemeVideo": "Video Chủ Đề",
"NameExtraFeaturette": "Nội dung phụ",
"NameExtraClip": "Clip ngắn",
"NameExtraTrailer": "Đoạn giới thiệu",
"NameExtraUnknown": "Nội dung bổ sung"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "用户数据清理任务",
"CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。",
"LyricDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的歌词",
"Original": "原始"
"Original": "原始",
"NameExtraBehindTheScenes": "幕后花絮",
"NameExtraClip": "片段",
"NameExtraDeletedScene": "删减场景",
"NameExtraFeaturette": "花絮",
"NameExtraInterview": "采访",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "试看",
"NameExtraScene": "精选片段",
"NameExtraShort": "短篇",
"NameExtraThemeSong": "主题曲",
"NameExtraThemeVideo": "主题视频",
"NameExtraTrailer": "预告片",
"NameExtraUnknown": "额外内容"
}
@@ -10,8 +10,8 @@
"Folders": "資料夾",
"Genres": "風格",
"HeaderContinueWatching": "繼續睇返",
"HeaderFavoriteEpisodes": "心水劇集",
"HeaderFavoriteShows": "心水節目",
"HeaderFavoriteEpisodes": "心水劇集",
"HeaderFavoriteShows": "心水節目",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "跟住落嚟",
"HomeVideos": "家庭影片",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "清理使用者資料嘅任務",
"CleanupUserDataTaskDescription": "清理已消失至少 90 日嘅媒體用家數據(包括觀看狀態、心水狀態等)。",
"LyricDownloadFailureFromForItem": "冇辦法從 {0} 下載 {1} 嘅歌詞",
"Original": "原始"
"Original": "原始",
"NameExtraBehindTheScenes": "幕後花絮",
"NameExtraClip": "片段",
"NameExtraDeletedScene": "刪減場景",
"NameExtraFeaturette": "花絮",
"NameExtraInterview": "采訪",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "試看",
"NameExtraScene": "精選片段",
"NameExtraShort": "短篇",
"NameExtraThemeSong": "主題曲",
"NameExtraThemeVideo": "主題影片",
"NameExtraTrailer": "預告片",
"NameExtraUnknown": "額外內容"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "用戶資料清理工作",
"CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。",
"Original": "原作",
"LyricDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的歌詞"
"LyricDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的歌詞",
"NameExtraBehindTheScenes": "幕後花絮",
"NameExtraClip": "片段",
"NameExtraDeletedScene": "刪減場景",
"NameExtraFeaturette": "花絮",
"NameExtraInterview": "采訪",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "試看",
"NameExtraScene": "精選片段",
"NameExtraShort": "短篇",
"NameExtraThemeSong": "主題曲",
"NameExtraThemeVideo": "主題影片",
"NameExtraTrailer": "預告片",
"NameExtraUnknown": "額外內容"
}
@@ -255,6 +255,14 @@ namespace Emby.Server.Implementations.Plugins
}
_plugins.Add(plugin);
// Updating a disabled plugin must not enable it again.
if (plugin.Manifest.Status == PluginStatus.Disabled)
{
ProcessAlternative(plugin);
return;
}
EnablePlugin(plugin);
}
@@ -387,29 +395,11 @@ namespace Emby.Server.Implementations.Plugins
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
var fileStream = AsyncFile.OpenWrite(imagePath);
Stream? downloadStream = null;
try
// The catalog is refreshed on every dashboard visit and rewrites the manifest of
// every installed plugin, so only fetch an image that is actually missing.
if (!ImageExists(imagePath))
{
downloadStream = await HttpClientFactory
.CreateClient(NamedClient.Default)
.GetStreamAsync(url)
.ConfigureAwait(false);
await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
imagePath = string.Empty;
}
finally
{
await fileStream.DisposeAsync().ConfigureAwait(false);
if (downloadStream is not null)
{
await downloadStream.DisposeAsync().ConfigureAwait(false);
}
imagePath = await DownloadImage(url, imagePath).ConfigureAwait(false);
}
}
@@ -448,6 +438,67 @@ namespace Emby.Server.Implementations.Plugins
}
}
private static bool ImageExists(string imagePath)
{
var image = new FileInfo(imagePath);
// A previous download may have been interrupted, leaving an empty file behind.
return image.Exists && image.Length > 0;
}
private async Task<string> DownloadImage(Uri url, string imagePath)
{
// Download to a temporary file and move it into place, so that neither a failed download
// nor a concurrent one can be observed as a partially written image.
var tempPath = imagePath + "." + Path.GetRandomFileName();
try
{
var fileStream = AsyncFile.Create(tempPath);
Stream? downloadStream = null;
try
{
downloadStream = await HttpClientFactory
.CreateClient(NamedClient.Default)
.GetStreamAsync(url)
.ConfigureAwait(false);
await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
finally
{
await fileStream.DisposeAsync().ConfigureAwait(false);
if (downloadStream is not null)
{
await downloadStream.DisposeAsync().ConfigureAwait(false);
}
}
File.Move(tempPath, imagePath, true);
return imagePath;
}
catch (Exception ex) when (ex is HttpRequestException or IOException or UnauthorizedAccessException)
{
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
TryDeleteFile(tempPath);
return string.Empty;
}
}
private void TryDeleteFile(string path)
{
try
{
File.Delete(path);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
_logger.LogWarning(ex, "Unable to delete {Path}.", path);
}
}
/// <summary>
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
/// If no file is found, no reconciliation occurs.
@@ -632,9 +683,10 @@ namespace Emby.Server.Implementations.Plugins
return;
}
var predecessor = _plugins.OrderByDescending(p => p.Version)
.FirstOrDefault(p => p.Id.Equals(plugin.Id) && p.IsEnabledAndSupported && p.Version != plugin.Version);
if (predecessor is not null)
var successor = _plugins.FirstOrDefault(p => p.Id.Equals(plugin.Id)
&& p.Version > plugin.Version
&& (p.IsEnabledAndSupported || p.Manifest.Status == PluginStatus.Disabled));
if (successor is not null)
{
return;
}
@@ -763,6 +815,8 @@ namespace Emby.Server.Implementations.Plugins
var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{
lastName = string.Empty;
if (!TryGetPluginDlls(entry, out var allowedDlls))
{
_logger.LogError("One or more assembly paths was invalid. Marking plugin {Plugin} as \"Malfunctioned\".", entry.Name);
@@ -772,15 +826,18 @@ namespace Emby.Server.Implementations.Plugins
entry.DllFiles = allowedDlls;
// Only clean up older versions when this version will actually be loaded.
if (entry.IsEnabledAndSupported)
{
lastName = entry.Name;
continue;
}
continue;
}
if (string.IsNullOrEmpty(lastName))
{
// Unnamed plugin, so there is nothing to match older versions against.
continue;
}
@@ -891,9 +948,9 @@ namespace Emby.Server.Implementations.Plugins
if (previousVersion is null)
{
// This value is memory only - so that the web will show restart required.
plugin.Manifest.Status = PluginStatus.Restart;
plugin.Manifest.AutoUpdate = false;
// Memory only, so that the web will show restart required. The manifest must keep
// holding the persisted state, or a later save would write the wrong state to disk.
plugin.RestartRequired = true;
return;
}
@@ -906,9 +963,7 @@ namespace Emby.Server.Implementations.Plugins
_logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
// This value is memory only - so that the web will show restart required.
plugin.Manifest.Status = PluginStatus.Restart;
plugin.Manifest.AutoUpdate = false;
plugin.RestartRequired = true;
}
}
}
@@ -174,7 +174,7 @@ public partial class AudioNormalizationTask : IScheduledTask
if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol)
{
t.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.EscapeProcessArgument()),
false,
cancellationToken).ConfigureAwait(false);
toSaveDbItems.Add(t);
@@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
/// <summary>
/// Optimizes Jellyfin's database by issuing a VACUUM command.
/// Optimizes Jellyfin's database by issuing VACUUM and ANALYZE commands.
/// </summary>
public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
{
@@ -82,7 +82,7 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
return;
}
_logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
_logger.LogInformation("Vacuuming and analyzing jellyfin.db...");
try
{
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library.Validators;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
@@ -29,6 +30,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
private readonly IFileSystem _fileSystem;
private readonly ILogger<PeopleValidationTask> _logger;
private readonly ILogger<PeopleValidator> _validatorLogger;
private readonly IItemTypeLookup _itemTypeLookup;
/// <summary>
@@ -39,6 +41,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
/// <param name="dbContextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{PeopleValidationTask}"/> interface.</param>
/// <param name="validatorLogger">Instance of the <see cref="ILogger{PeopleValidator}"/> interface.</param>
/// <param name="itemTypeLookup">Instance of the <see cref="IItemTypeLookup"/> interface.</param>
public PeopleValidationTask(
ILibraryManager libraryManager,
@@ -46,6 +49,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
IDbContextFactory<JellyfinDbContext> dbContextFactory,
IFileSystem fileSystem,
ILogger<PeopleValidationTask> logger,
ILogger<PeopleValidator> validatorLogger,
IItemTypeLookup itemTypeLookup)
{
_libraryManager = libraryManager;
@@ -53,6 +57,7 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
_dbContextFactory = dbContextFactory;
_fileSystem = fileSystem;
_logger = logger;
_validatorLogger = validatorLogger;
_itemTypeLookup = itemTypeLookup;
}
@@ -109,6 +114,8 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
var dupQuery = context.Peoples
.GroupBy(e => new { e.Name, e.PersonType })
.Where(e => e.Count() > 1)
.OrderBy(e => e.Key.Name)
.ThenBy(e => e.Key.PersonType)
.Select(e => e.Select(f => f.Id).ToArray());
var total = dupQuery.Count();
@@ -163,7 +170,9 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
// Phase 2: Validate people (33-66%). Runs after orphaned PeopleBaseItemMap entries are
// cleaned up above, so dead people are removed in a single pass instead of requiring a second run.
IProgress<double> validateProgress = new Progress<double>((val) => progress.Report((val / 3) + 33));
await _libraryManager.ValidatePeopleAsync(validateProgress, cancellationToken).ConfigureAwait(false);
await new PeopleValidator(_libraryManager, _validatorLogger)
.Run(validateProgress, cancellationToken)
.ConfigureAwait(false);
// Phase 3: Refresh images for people missing them (66-100%)
IProgress<double> refreshProgress = new Progress<double>((val) => progress.Report((val / 3) + 66));
@@ -177,33 +186,14 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30);
var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
List<Guid> peopleIds;
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
const int PartitionSize = 100;
var numPeople = await context.BaseItems
.AsNoTracking()
.Where(b => b.Type == personTypeName)
.Where(b => b.DateLastRefreshed == null || b.DateLastRefreshed < thirtyDaysAgo)
.Where(b =>
!b.Images!.Any(i => i.ImageType == ImageInfoImageType.Primary) ||
string.IsNullOrEmpty(b.Overview))
.CountAsync(cancellationToken)
.ConfigureAwait(false);
_logger.LogDebug("Found {Count} people needing image/overview refresh", numPeople);
if (numPeople == 0)
{
progress.Report(100);
return;
}
var numComplete = 0;
var numRefreshed = 0;
await foreach (var entry in context.BaseItems
// Read the candidates in one go rather than paging them. A refresh stamps the person and takes
// it out of this set, so a growing offset over a shrinking set walks past people it never visits.
peopleIds = await context.BaseItems
.AsNoTracking()
.Where(b => b.Type == personTypeName)
.Where(b => b.DateLastRefreshed == null || b.DateLastRefreshed < thirtyDaysAgo)
@@ -211,22 +201,36 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
!b.Images!.Any(i => i.ImageType == ImageInfoImageType.Primary) ||
string.IsNullOrEmpty(b.Overview))
.OrderBy(b => b.Id)
.WithPartitionProgress(partition => _logger.LogDebug("Processing people partition {Partition}", partition))
.PartitionEagerAsync(PartitionSize, cancellationToken)
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
if (await RefreshPersonAsync(entry.Id, cancellationToken).ConfigureAwait(false))
{
numRefreshed++;
}
.Select(b => b.Id)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
}
numComplete++;
progress.Report(100.0 * numComplete / numPeople);
_logger.LogDebug("Found {Count} people needing image/overview refresh", peopleIds.Count);
if (peopleIds.Count == 0)
{
progress.Report(100);
return;
}
var numComplete = 0;
var numRefreshed = 0;
foreach (var personId in peopleIds)
{
cancellationToken.ThrowIfCancellationRequested();
if (await RefreshPersonAsync(personId, cancellationToken).ConfigureAwait(false))
{
numRefreshed++;
}
_logger.LogInformation("Refreshed metadata for {Count} people missing images or overview", numRefreshed);
numComplete++;
progress.Report(100.0 * numComplete / peopleIds.Count);
}
_logger.LogInformation("Refreshed metadata for {Count} people missing images or overview", numRefreshed);
}
private async Task<bool> RefreshPersonAsync(Guid personId, CancellationToken cancellationToken)
@@ -243,8 +247,8 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
ImageRefreshMode = hasImage ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default,
MetadataRefreshMode = hasOverview ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default
ImageRefreshMode = hasImage ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.FullRefresh,
MetadataRefreshMode = hasOverview ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.FullRefresh
};
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
@@ -107,6 +107,11 @@ public class PluginUpdateTask : IScheduledTask, IConfigurableScheduledTask
{
_logger.LogError(ex, "Error updating {Name}", package.Name);
}
catch (TimeoutException ex)
{
// One slow download must not abort the updates for the remaining plugins.
_logger.LogError(ex, "Error downloading {Name}", package.Name);
}
catch (InvalidDataException ex)
{
_logger.LogError(ex, "Error updating {Name}", package.Name);
@@ -309,7 +309,7 @@ namespace Emby.Server.Implementations.Session
{
if (!session.SessionControllers.Any(i => i.IsSessionActive))
{
var key = GetSessionKey(session.Client, session.DeviceId);
var key = GetSessionKey(session.Client, session.DeviceId, session.UserId);
_activeConnections.TryRemove(key, out _);
if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
@@ -369,7 +369,7 @@ namespace Emby.Server.Implementations.Session
if (session is not null)
{
var key = GetSessionKey(session.Client, session.DeviceId);
var key = GetSessionKey(session.Client, session.DeviceId, session.UserId);
_activeConnections.TryRemove(key, out _);
@@ -475,8 +475,11 @@ namespace Emby.Server.Implementations.Session
}
}
private static string GetSessionKey(string appName, string deviceId)
=> appName + deviceId;
// The user is part of the key because the client name and the device id are taken from the
// request headers and are not bound to the access token. Without it, any authenticated user
// could claim another user's client/device pair and take over their session.
private static string GetSessionKey(string appName, string deviceId, Guid userId)
=> appName + deviceId + userId.ToString("N", CultureInfo.InvariantCulture);
/// <summary>
/// Gets the connection.
@@ -500,7 +503,7 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(deviceId);
var key = GetSessionKey(appName, deviceId);
var key = GetSessionKey(appName, deviceId, user?.Id ?? Guid.Empty);
SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
if (ReferenceEquals(newSession, sessionInfo))
@@ -655,7 +658,7 @@ namespace Emby.Server.Implementations.Session
ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
SessionId = session.Id,
MediaSourceId = session.PlayState?.MediaSourceId,
PositionTicks = session.PlayState?.PositionTicks
PositionTicks = session.LastPlaybackCheckInPositionTicks
}).ConfigureAwait(false);
}
catch (Exception ex)
@@ -1537,11 +1540,52 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
}
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
private void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
{
ArgumentNullException.ThrowIfNull(session);
ArgumentNullException.ThrowIfNull(controllingSession);
var controllingUserId = controllingSession.UserId;
// Controlling a session is always allowed when:
// - the caller has no associated user (an API key, which is a privileged context),
// - the target session is public (has no owning user), or
// - the caller's user is associated with the target session.
// Controlling a session owned by a different user requires the
// EnableRemoteControlOfOtherUsers permission.
if (controllingUserId.IsEmpty()
|| session.UserId.IsEmpty()
|| session.ContainsUser(controllingUserId))
{
return;
}
var controllingUser = _userManager.GetUserById(controllingUserId);
if (controllingUser is null
|| !controllingUser.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
{
throw new SecurityException("The current user does not have permission to remote control other users.");
}
}
private void AssertCanAttachUser(SessionInfo controllingSession, Guid userId)
{
var controllingUserId = controllingSession.UserId;
// Playback reported by a session is also written to the user data of its additional users,
// so attaching anyone but the calling user requires administrative privileges.
if (controllingUserId.IsEmpty() || controllingUserId.Equals(userId))
{
return;
}
var controllingUser = _userManager.GetUserById(controllingUserId);
if (controllingUser is null
|| !controllingUser.HasPermission(PermissionKind.IsAdministrator))
{
throw new SecurityException("The current user does not have permission to attach another user to a session.");
}
}
/// <summary>
@@ -1559,16 +1603,24 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Adds the additional user.
/// </summary>
/// <param name="controllingSessionId">The controlling session identifier.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="userId">The user identifier.</param>
/// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</exception>
/// <exception cref="SecurityException">The controlling user is not allowed to attach the user to the session.</exception>
/// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exception>
public void AddAdditionalUser(string sessionId, Guid userId)
public void AddAdditionalUser(string controllingSessionId, string sessionId, Guid userId)
{
CheckDisposed();
var session = GetSession(sessionId);
if (!string.IsNullOrEmpty(controllingSessionId))
{
var controllingSession = GetSession(controllingSessionId);
AssertCanControl(session, controllingSession);
AssertCanAttachUser(controllingSession, userId);
}
if (session.UserId.Equals(userId))
{
throw new ArgumentException("The requested user is already the primary user of the session.");
@@ -1576,7 +1628,8 @@ namespace Emby.Server.Implementations.Session
if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
{
var user = _userManager.GetUserById(userId);
var user = _userManager.GetUserById(userId)
?? throw new ArgumentException("The requested user does not exist.");
var newUser = new SessionUserInfo
{
UserId = userId,
@@ -1590,16 +1643,22 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Removes the additional user.
/// </summary>
/// <param name="controllingSessionId">The controlling session identifier.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="userId">The user identifier.</param>
/// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</exception>
/// <exception cref="SecurityException">The controlling user is not allowed to control the session.</exception>
/// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exception>
public void RemoveAdditionalUser(string sessionId, Guid userId)
public void RemoveAdditionalUser(string controllingSessionId, string sessionId, Guid userId)
{
CheckDisposed();
var session = GetSession(sessionId);
if (!string.IsNullOrEmpty(controllingSessionId))
{
AssertCanControl(session, GetSession(controllingSessionId));
}
if (session.UserId.Equals(userId))
{
throw new ArgumentException("The requested user is already the primary user of the session.");
@@ -1803,14 +1862,21 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Reports the capabilities.
/// </summary>
/// <param name="controllingSessionId">The controlling session identifier.</param>
/// <param name="sessionId">The session identifier.</param>
/// <param name="capabilities">The capabilities.</param>
public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
/// <exception cref="SecurityException">The controlling user is not allowed to control the session.</exception>
public void ReportCapabilities(string controllingSessionId, string sessionId, ClientCapabilities capabilities)
{
CheckDisposed();
var session = GetSession(sessionId);
if (!string.IsNullOrEmpty(controllingSessionId))
{
AssertCanControl(session, GetSession(controllingSessionId));
}
ReportCapabilities(session, capabilities, true);
}
@@ -1905,13 +1971,18 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public void ReportNowViewingItem(string sessionId, string itemId)
public void ReportNowViewingItem(string controllingSessionId, string sessionId, string itemId)
{
ArgumentException.ThrowIfNullOrEmpty(itemId);
var item = _libraryManager.GetItemById(new Guid(itemId));
var session = GetSession(sessionId);
if (!string.IsNullOrEmpty(controllingSessionId))
{
AssertCanControl(session, GetSession(controllingSessionId));
}
session.NowViewingItem = GetItemInfo(item, null);
}
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.Session
if (inactive.Count > 0)
{
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
_logger.LogDebug("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
}
foreach (var webSocket in inactive)
+16 -2
View File
@@ -90,6 +90,18 @@ namespace Emby.Server.Implementations.SyncPlay
/// <value>The default ping.</value>
public long DefaultPing { get; } = 500;
/// <summary>
/// Gets the maximum ping, in milliseconds, accepted from a session.
/// </summary>
/// <remarks>
/// Pings are reported by clients and are scaled into the delays used to schedule playback,
/// so an unbounded value lets a single session push the whole group's resume point
/// arbitrarily far out, or overflow the arithmetic entirely. Anything above this is not a
/// usable measurement for synchronisation.
/// </remarks>
/// <value>The maximum ping.</value>
public long MaxPing { get; } = 10000;
/// <summary>
/// Gets the maximum time offset error accepted for dates reported by clients, in milliseconds.
/// </summary>
@@ -438,7 +450,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
if (_participants.TryGetValue(session.Id, out GroupMember value))
{
value.Ping = ping;
value.Ping = Math.Clamp(ping, 0, MaxPing);
}
}
@@ -451,7 +463,9 @@ namespace Emby.Server.Implementations.SyncPlay
max = Math.Max(max, session.Ping);
}
return max;
// A group with no participants has no ping to report. Returning long.MinValue would
// overflow the callers that scale this value into ticks, so fall back to the default.
return max == long.MinValue ? DefaultPing : max;
}
/// <inheritdoc />
@@ -181,8 +181,8 @@ namespace Emby.Server.Implementations.SyncPlay
{
if (existingGroup.GroupId.Equals(request.GroupId))
{
// Restore session.
UpdateSessionsCounter(session.UserId, 1);
// Restore session. The session is already in the group and has already
// been counted, so the counter must not be incremented a second time.
group.SessionJoin(session, request, cancellationToken);
return;
}
@@ -332,8 +332,11 @@ namespace Emby.Server.Implementations.SyncPlay
// Group lock required as Group is not thread-safe.
lock (group)
{
// Make sure that session still belongs to this group.
if (_sessionToGroupMap.TryGetValue(session.Id, out var checkGroup) && !checkGroup.GroupId.Equals(group.GroupId))
// Make sure that session still belongs to this group. The lookup can fail
// outright when the session left while this request was waiting on the group
// lock, which is exactly the case this re-check exists to catch.
if (!_sessionToGroupMap.TryGetValue(session.Id, out var checkGroup)
|| !checkGroup.GroupId.Equals(group.GroupId))
{
// Drop request.
return;
@@ -400,7 +403,7 @@ namespace Emby.Server.Implementations.SyncPlay
// Update sessions counter.
var newSessionsCounter = _activeUsers.AddOrUpdate(
userId,
1,
toAdd,
(_, sessionsCounter) => sessionsCounter + toAdd);
// Should never happen.
@@ -11,7 +11,6 @@ using System.Security.Cryptography;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
@@ -34,6 +33,9 @@ namespace Emby.Server.Implementations.Updates
public class InstallationManager : IInstallationManager
{
private static readonly SearchValues<char> InvalidPackageNameChars = SearchValues.Create([.. Path.GetInvalidFileNameChars(), '/', '\\']);
// Budget for the whole package download. The response headers are already bounded by the
// HttpClient timeout; this covers reading the package body, which can be large and slow.
private static readonly TimeSpan PackageDownloadTimeout = TimeSpan.FromMinutes(10);
/// <summary>
/// The logger.
@@ -82,8 +84,8 @@ namespace Emby.Server.Implementations.Updates
IServerConfigurationManager config,
IPluginManager pluginManager)
{
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
_currentInstallations = [];
_completedInstallationsInternal = [];
_logger = logger;
_applicationHost = appHost;
@@ -341,8 +343,9 @@ namespace Emby.Server.Implementations.Updates
_applicationHost.NotifyPendingRestart();
}
catch (OperationCanceledException)
catch (OperationCanceledException) when (linkedToken.IsCancellationRequested)
{
// Only an actually cancelled token is a cancellation.
lock (_currentInstallationsLock)
{
_currentInstallations.Remove(tuple);
@@ -356,7 +359,7 @@ namespace Emby.Server.Implementations.Updates
}
catch (Exception ex)
{
_logger.LogError(ex, "Package installation failed");
_logger.LogError(ex, "Package installation failed: {Name} {Version}", package.Name, package.Version);
lock (_currentInstallationsLock)
{
@@ -500,8 +503,9 @@ namespace Emby.Server.Implementations.Updates
var plugins = _pluginManager.Plugins;
foreach (var plugin in plugins)
{
// Don't auto update when plugin marked not to, or when it's disabled.
if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
// Don't auto update when plugin marked not to, or when it's disabled or pending removal.
if (plugin.Manifest?.AutoUpdate == false
|| plugin.Manifest?.Status is PluginStatus.Disabled or PluginStatus.Deleted)
{
continue;
}
@@ -545,12 +549,36 @@ namespace Emby.Server.Implementations.Updates
throw new InvalidDataException($"Plugin package name '{package.Name}' resolves outside the plugins directory.");
}
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
// ResponseHeadersRead keeps the body out of the HttpClient timeout, which otherwise covers
// the whole download; the package gets the longer budget below instead.
using var downloadTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
downloadTokenSource.CancelAfter(PackageDownloadTimeout);
var downloadToken = downloadTokenSource.Token;
var buffer = new MemoryStream();
await using (buffer.ConfigureAwait(false))
{
try
{
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(new Uri(package.SourceUrl), HttpCompletionOption.ResponseHeadersRead, downloadToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
// The package is read twice, for the checksum and for the extraction, so it has
// to be buffered: the response stream is not seekable.
await response.Content.CopyToAsync(buffer, downloadToken).ConfigureAwait(false);
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
// Either our budget above or the HttpClient timeout ran out.
throw new TimeoutException(
$"Downloading the package {package.Name} {package.Version} from {package.SourceUrl} timed out.",
ex);
}
buffer.Position = 0;
Stream stream = buffer;
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
cancellationToken.ThrowIfCancellationRequested();
+14 -50
View File
@@ -126,6 +126,12 @@ public class ArtistsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
// Asking for a type filter has always implied wanting that type's counts back.
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
{
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
}
User? user = null;
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
@@ -193,31 +199,7 @@ public class ArtistsController : BaseJellyfinApiController
var result = _libraryManager.GetArtists(query);
var dtos = result.Items.Select(i =>
{
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (includeItemTypes.Length != 0)
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
dto.SeriesCount = itemCounts.SeriesCount;
dto.EpisodeCount = itemCounts.EpisodeCount;
dto.MovieCount = itemCounts.MovieCount;
dto.TrailerCount = itemCounts.TrailerCount;
dto.AlbumCount = itemCounts.AlbumCount;
dto.SongCount = itemCounts.SongCount;
dto.ArtistCount = itemCounts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>(
query.StartIndex,
result.TotalRecordCount,
dtos.ToArray());
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
}
/// <summary>
@@ -298,6 +280,12 @@ public class ArtistsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
// Asking for a type filter has always implied wanting that type's counts back.
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
{
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
}
User? user = null;
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
@@ -365,31 +353,7 @@ public class ArtistsController : BaseJellyfinApiController
var result = _libraryManager.GetAlbumArtists(query);
var dtos = result.Items.Select(i =>
{
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (includeItemTypes.Length != 0)
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
dto.SeriesCount = itemCounts.SeriesCount;
dto.EpisodeCount = itemCounts.EpisodeCount;
dto.MovieCount = itemCounts.MovieCount;
dto.TrailerCount = itemCounts.TrailerCount;
dto.AlbumCount = itemCounts.AlbumCount;
dto.SongCount = itemCounts.SongCount;
dto.ArtistCount = itemCounts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>(
query.StartIndex,
result.TotalRecordCount,
dtos.ToArray());
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
}
/// <summary>
@@ -20,7 +20,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
@@ -1456,22 +1455,16 @@ public class DynamicHlsController : BaseJellyfinApiController
var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
TranscodingJob? job;
if (System.IO.File.Exists(segmentPath))
{
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
_logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
// Keep segment selection and transcoding replacement under the same playlist lock.
// An out-of-order request must not replace a job while another request is using its output.
using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
{
TranscodingJob? job;
var startTranscoding = false;
if (System.IO.File.Exists(segmentPath))
{
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
_logger.LogDebug("returning {0} [it exists]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@@ -1505,6 +1498,9 @@ public class DynamicHlsController : BaseJellyfinApiController
// If the playlist doesn't already exist, startup ffmpeg
try
{
var currentJob = _transcodeManager.GetTranscodingJob(playlistPath, TranscodingJobType);
await WaitForActiveTranscodingRequests(currentJob, cancellationToken).ConfigureAwait(false);
await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
.ConfigureAwait(false);
@@ -1540,11 +1536,19 @@ public class DynamicHlsController : BaseJellyfinApiController
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
}
}
}
_logger.LogDebug("returning {0} [general case]", segmentPath);
job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("returning {0} [general case]", segmentPath);
job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
}
internal static async Task WaitForActiveTranscodingRequests(TranscodingJob? job, CancellationToken cancellationToken)
{
while (job?.ActiveRequestCount > 0)
{
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
}
private static double[] GetSegmentLengths(StreamState state)
@@ -1647,9 +1651,9 @@ public class DynamicHlsController : BaseJellyfinApiController
segmentFormat,
startNumber.ToString(CultureInfo.InvariantCulture),
baseUrlParam,
EncodingUtils.NormalizePath(outputTsArg),
outputTsArg.EscapeProcessArgument(),
hlsArguments,
EncodingUtils.NormalizePath(outputPath)).Trim();
outputPath.EscapeProcessArgument()).Trim();
}
/// <summary>
+7 -2
View File
@@ -97,6 +97,12 @@ public class GenresController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
// Asking for a type filter has always implied wanting that type's counts back.
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
{
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
}
User? user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -143,8 +149,7 @@ public class GenresController : BaseJellyfinApiController
result = _libraryManager.GetGenres(query);
}
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
}
/// <summary>
@@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
@@ -263,7 +264,7 @@ public class ItemLookupController : BaseJellyfinApiController
searchResult.ProviderIds);
// Since the refresh process won't erase provider Ids, we need to set this explicitly now.
item.ProviderIds = searchResult.ProviderIds;
item.SetProviderIds(searchResult.ProviderIds);
await _providerManager.RefreshFullItem(
item,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
@@ -428,15 +428,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (request.ProviderIds is not null)
{
foreach (var pair in request.ProviderIds.ToList())
{
if (string.IsNullOrEmpty(pair.Value))
{
request.ProviderIds.Remove(pair.Key);
}
}
item.ProviderIds = request.ProviderIds;
item.SetProviderIds(request.ProviderIds);
}
if (item is Video video)
@@ -14,7 +14,9 @@ using MediaBrowser.Common.Api;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization;
@@ -33,6 +35,7 @@ public class LibraryStructureController : BaseJellyfinApiController
private readonly IServerApplicationPaths _appPaths;
private readonly ILibraryManager _libraryManager;
private readonly ILibraryMonitor _libraryMonitor;
private readonly IDirectoryService _directoryService;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
@@ -40,14 +43,17 @@ public class LibraryStructureController : BaseJellyfinApiController
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
/// <param name="directoryService">Instance of <see cref="IDirectoryService"/> interface.</param>
public LibraryStructureController(
IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager,
ILibraryMonitor libraryMonitor)
ILibraryMonitor libraryMonitor,
IDirectoryService directoryService)
{
_appPaths = serverConfigurationManager.ApplicationPaths;
_libraryManager = libraryManager;
_libraryMonitor = libraryMonitor;
_directoryService = directoryService;
}
/// <summary>
@@ -122,12 +128,14 @@ public class LibraryStructureController : BaseJellyfinApiController
/// <param name="newName">The new name.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <response code="204">Folder renamed.</response>
/// <response code="400">The new name is not a valid library name.</response>
/// <response code="404">Library doesn't exist.</response>
/// <response code="409">Library already exists.</response>
/// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns>
/// <returns>A <see cref="NoContentResult"/> on success, a <see cref="BadRequestResult"/> if the new name is invalid, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns>
/// <exception cref="ArgumentNullException">The new name may not be null.</exception>
[HttpPost("Name")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public ActionResult RenameVirtualFolder(
@@ -147,10 +155,15 @@ public class LibraryStructureController : BaseJellyfinApiController
var rootFolderPath = _appPaths.DefaultUserViewsPath;
var currentPath = Path.Combine(rootFolderPath, name);
var newPath = Path.Combine(rootFolderPath, newName);
// Both names are caller supplied, so they have to be confined to the libraries root.
var newPath = FileSystemHelper.GetChildPath(rootFolderPath, newName);
if (newPath is null)
{
return BadRequest("The new name is not a valid library name.");
}
if (!Directory.Exists(currentPath))
var currentPath = FileSystemHelper.GetChildPath(rootFolderPath, name);
if (currentPath is null || !Directory.Exists(currentPath))
{
return NotFound("The media collection does not exist.");
}
@@ -170,11 +183,11 @@ public class LibraryStructureController : BaseJellyfinApiController
var tempPath = Path.Combine(
rootFolderPath,
Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
Directory.Move(currentPath, tempPath);
_directoryService.Move(currentPath, tempPath);
currentPath = tempPath;
}
Directory.Move(currentPath, newPath);
_directoryService.Move(currentPath, newPath);
}
finally
{
@@ -98,6 +98,12 @@ public class MusicGenresController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
// Asking for a type filter has always implied wanting that type's counts back.
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
{
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
}
User? user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -134,8 +140,7 @@ public class MusicGenresController : BaseJellyfinApiController
var result = _libraryManager.GetMusicGenres(query);
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
}
/// <summary>
+30 -1
View File
@@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates;
@@ -23,16 +25,22 @@ public class PackageController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IPluginManager _pluginManager;
/// <summary>
/// Initializes a new instance of the <see cref="PackageController"/> class.
/// </summary>
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager)
/// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
public PackageController(
IInstallationManager installationManager,
IServerConfigurationManager serverConfigurationManager,
IPluginManager pluginManager)
{
_installationManager = installationManager;
_serverConfigurationManager = serverConfigurationManager;
_pluginManager = pluginManager;
}
/// <summary>
@@ -48,6 +56,13 @@ public class PackageController : BaseJellyfinApiController
[FromRoute, Required] string name,
[FromQuery] Guid? assemblyGuid)
{
// Plugins bundled with the server are not published to any repository, so querying
// the configured repositories for them can only ever fail, and does so slowly.
if (IsBundledPlugin(name, assemblyGuid))
{
return NotFound();
}
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
var result = _installationManager.FilterPackages(
packages,
@@ -96,6 +111,11 @@ public class PackageController : BaseJellyfinApiController
[FromQuery] string? version,
[FromQuery] string? repositoryUrl)
{
if (IsBundledPlugin(name, assemblyGuid))
{
return NotFound();
}
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
if (!string.IsNullOrEmpty(repositoryUrl))
{
@@ -161,4 +181,13 @@ public class PackageController : BaseJellyfinApiController
_serverConfigurationManager.SaveConfiguration();
return NoContent();
}
private bool IsBundledPlugin(string name, Guid? assemblyGuid)
{
var plugin = assemblyGuid is Guid id && !id.IsEmpty()
? _pluginManager.GetPlugin(id)
: _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
return plugin?.Instance?.CanUninstall == false;
}
}
@@ -4,6 +4,7 @@ using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
@@ -103,6 +104,7 @@ public class PersonsController : BaseJellyfinApiController
personTypes,
excludePersonTypes)
{
AccessFilter = BuildAccessFilter(user),
NameContains = searchTerm,
NameStartsWith = nameStartsWith,
NameLessThan = nameLessThan,
@@ -123,6 +125,20 @@ public class PersonsController : BaseJellyfinApiController
.ToArray());
}
// People are not owned by a library, so nothing in the Peoples table says which of them a user is
// allowed to see; that only follows from the items they are credited on.
private InternalItemsQuery? BuildAccessFilter(User? user)
{
if (user is null || !user.HasContentRestrictions())
{
return null;
}
var accessFilter = new InternalItemsQuery(user) { IncludeOwnedItems = true };
_libraryManager.ConfigureUserAccess(accessFilter, user);
return accessFilter;
}
/// <summary>
/// Get person by name.
/// </summary>
@@ -521,7 +521,7 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
var callingUserId = userId ?? User.GetUserId();
var callingUserId = RequestHelpers.GetUserId(User, userId);
var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
if (playlist is null)
{
+18 -10
View File
@@ -306,11 +306,14 @@ public class SessionController : BaseJellyfinApiController
[HttpPost("Sessions/{sessionId}/User/{userId}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddUserToSession(
public async Task<ActionResult> AddUserToSession(
[FromRoute, Required] string sessionId,
[FromRoute, Required] Guid userId)
{
_sessionManager.AddAdditionalUser(sessionId, userId);
_sessionManager.AddAdditionalUser(
await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
sessionId,
userId);
return NoContent();
}
@@ -324,11 +327,14 @@ public class SessionController : BaseJellyfinApiController
[HttpDelete("Sessions/{sessionId}/User/{userId}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveUserFromSession(
public async Task<ActionResult> RemoveUserFromSession(
[FromRoute, Required] string sessionId,
[FromRoute, Required] Guid userId)
{
_sessionManager.RemoveAdditionalUser(sessionId, userId);
_sessionManager.RemoveAdditionalUser(
await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
sessionId,
userId);
return NoContent();
}
@@ -352,12 +358,13 @@ public class SessionController : BaseJellyfinApiController
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsPersistentIdentifier = true)
{
var currentSessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(id))
{
id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
id = currentSessionId;
}
_sessionManager.ReportCapabilities(id, new ClientCapabilities
_sessionManager.ReportCapabilities(currentSessionId, id, new ClientCapabilities
{
PlayableMediaTypes = playableMediaTypes,
SupportedCommands = supportedCommands,
@@ -381,12 +388,13 @@ public class SessionController : BaseJellyfinApiController
[FromQuery] string? id,
[FromBody, Required] ClientCapabilitiesDto capabilities)
{
var currentSessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(id))
{
id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
id = currentSessionId;
}
_sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
_sessionManager.ReportCapabilities(currentSessionId, id, capabilities.ToClientCapabilities());
return NoContent();
}
@@ -405,9 +413,9 @@ public class SessionController : BaseJellyfinApiController
[FromQuery] string? sessionId,
[FromQuery, Required] string? itemId)
{
string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var currentSessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
_sessionManager.ReportNowViewingItem(session, itemId);
_sessionManager.ReportNowViewingItem(currentSessionId, sessionId ?? currentSessionId, itemId);
return NoContent();
}
@@ -92,6 +92,12 @@ public class StudiosController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
// Asking for a type filter has always implied wanting that type's counts back.
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
{
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
}
User? user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -126,8 +132,7 @@ public class StudiosController : BaseJellyfinApiController
}
var result = _libraryManager.GetStudios(query);
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
}
/// <summary>
@@ -557,7 +557,7 @@ public class SubtitleController : BaseJellyfinApiController
if (!string.IsNullOrEmpty(fallbackFontPath))
{
var fontFile = _fileSystem.GetFiles(fallbackFontPath)
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
var fileSize = fontFile?.Length;
if (fontFile is not null && fileSize is not null && fileSize > 0)

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