Compare commits

..

448 Commits

Author SHA1 Message Date
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 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
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
Bas b3a0611302 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2026-08-02 20:18:33 +00:00
Gargotaire c20cc766e1 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/
2026-08-02 20:18:33 +00:00
Shadowghost 8044534774 Merge remote-tracking branch 'upstream/master' into fix-byname-queries 2026-08-02 22:12:57 +02:00
Bas cac463d4c3 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2026-08-02 18:49:57 +00:00
Bas 7faa35dab4 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2026-08-02 18:49:28 +00:00
Cody Robibero 24022092ad Merge pull request #17456 from Shadowghost/fix-extras
Fix extras naming and version assignment
2026-08-02 14:25:00 -04:00
Cody Robibero e2e3094d2d Merge pull request #17512 from altqx/libbitsub
Allow client-rendered graphical subtitles during remux
2026-08-02 14:24:51 -04:00
Cody Robibero d669994bf0 Merge pull request #17503 from theguymadmax/fix-sortby-name
Use CleanName when sorting by name
2026-08-02 14:24:28 -04:00
Cody Robibero 45b8f6c4c8 Merge pull request #17486 from Shadowghost/fix-adjacent
Fix AdjacentTo being ignored on non-recursive item queries
2026-08-02 14:24:13 -04:00
Cody Robibero 044f651299 Merge pull request #17482 from Shadowghost/fix-stale-versions
Fix video version links being read from stale serialised item data instead of the LinkedChildren table
2026-08-02 14:24:05 -04:00
Cody Robibero f865910a90 Merge pull request #17298 from WizardOfYendor1/fix/livetv-published-stream-urls
Fix Live TV returning unreachable "server-local" streaming URLs to clients.
2026-08-02 14:23:49 -04:00
Cody Robibero f2360c07a3 Merge pull request #17147 from Shadowghost/tmdb-missing-episodes
Add Tmdb missing episode provider
2026-08-02 14:23:30 -04:00
Shadowghost 4c812c9ba4 Switch to opt-in 2026-08-02 18:02:57 +02:00
krvi 26261dbfe7 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-02 01:08:15 +00:00
krvi ae90e0e52e Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-01 22:58:15 +00:00
krvi 72d0895401 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-01 22:10:09 +00: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
Weblate e16c8a07bd Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/
2026-08-01 18:21:52 +00:00
krvi a7a138db31 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-01 18:21:48 +00:00
krvi 76956c0abc Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-01 17:49:28 +00:00
altqx d4376e0539 Allow client-rendered graphical subtitles during remux 2026-08-01 22:49:34 +07:00
Shadowghost b94ba9d409 Merge remote-tracking branch 'upstream/master' into tmdb-missing-episodes
# Conflicts:
#	Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
2026-08-01 14:25:20 +02:00
Cody Robibero 744ca84a8b Merge pull request #17500 from theguymadmax/fix-storage-lookup
Fix storage info lookup for Windows
2026-08-01 08:18:45 -04:00
Cody Robibero c55fde25a5 Merge pull request #17501 from alchemyyy/fix/skip-sidx-fmp4-hls
Skip SIDX in fMP4 HLS segments
2026-08-01 08:07:42 -04:00
Cody Robibero 030031dcff Merge pull request #17463 from Shadowghost/fix-unplayed-filter
Fix (Un)Played filter correctness and performance
2026-08-01 08:07:05 -04:00
Cody Robibero 341c19bace Merge pull request #17460 from Shadowghost/fix-user-items
Queue person metadata refresh instead of blocking the item request and fix ItemCounts
2026-08-01 08:06:46 -04:00
Cody Robibero e3a8d209b4 Merge pull request #17455 from Shadowghost/fix-migrations
Reevaluate pending migrations after each one instead of per stage
2026-08-01 08:06:33 -04:00
Cody Robibero e816870f67 Merge pull request #17416 from Shadowghost/enable-duplicate-playlist-children
Allow duplicate LinkedChildren for Playlists
2026-08-01 08:06:02 -04:00
theguymadmax f28acc7fa1 Use CleanName when sorting by name 2026-07-31 00:34:43 -04:00
krvi b2ae39e0d9 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-07-31 02:19:10 +00:00
alchemyyy 893662ba50 Skip SIDX in fMP4 HLS segments
Prevent FFmpeg's SIDX path from rewriting open-GOP boundary packet timestamps. HLS uses the media playlist for segment indexing and does not require the SIDX box.
2026-07-30 17:19:45 -07:00
theguymadmax 314004bc19 Fix storage lookup for Windows 2026-07-30 19:13:32 -04: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
krvi b15d3c9ad3 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-07-30 02:32:30 +00:00
GOvEy1nw 3da05330b2 fix(images): disambiguate progress overlay cache keys 2026-07-29 17:37:36 +01:00
Shadowghost 8293eb26b9 Fix playlist entries being lost on migration and library scans 2026-07-29 14:40:49 +02:00
Shadowghost d8fc0a9914 Fix AdjacentTo being ignored on non-recursive item queries 2026-07-29 13:13:44 +02:00
Shadowghost 1f4f4acb46 Fixup 2026-07-29 09:39:04 +02:00
Shadowghost d64e18b69a Fix more filter cases 2026-07-29 09:35:20 +02:00
Shadowghost 7a4271c85f Fix video version links being read from stale serialised item data instead of the LinkedChildren table 2026-07-29 07:31:27 +02:00
Paroc afb716566d Translated using Weblate (Occitan)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/oc/
2026-07-29 00:32:44 +00:00
Cody Robibero e5c68e1257 Merge pull request #17424 from Shadowghost/add-audiodb-search
Implement AudioDb artist search
2026-07-28 19:15:20 -04:00
Shadowghost a94588497c Fix Folder access filtering 2026-07-28 23:13:10 +02:00
Shadowghost 4cedbe28e3 Apply review suggestion 2026-07-28 22:18:18 +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
Bond-009 9ff92f3f0d Merge pull request #17461 from jellyfin/renovate/actions-stale-11.x
Update actions/stale action to v11
2026-07-28 20:21:00 +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
Bond-009 d92e59aa72 Merge pull request #17459 from jellyfin/renovate/ci-deps
Update danielpalme/ReportGenerator-GitHub-Action action to v5.5.11
2026-07-28 20:11:33 +02:00
Bond-009 d593436281 Merge pull request #17448 from jellyfin/renovate/fscheck.xunit.v3-3.x
Update dependency FsCheck.Xunit.v3 to 3.3.4
2026-07-28 20:10:35 +02:00
Bond-009 41b8bd4459 Merge pull request #17310 from TowyTowy/fix/format3d-trailing-token
Fix 3D format detection when the tag is the last token of the path
2026-07-28 20:05:51 +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
David Wagener 7215950bd4 Translated using Weblate (Luxembourgish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lb/
2026-07-28 16:27:52 +00:00
Natawan Jongnetiwisit c435f63d09 Translated using Weblate (Thai)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/th/
2026-07-28 16:27:49 +00:00
Shadowghost eed664b7d3 Fix played/unplayed filter for empty Series and BoxSets 2026-07-28 12:43:12 +02:00
David Wagener 5b550517b2 Translated using Weblate (Luxembourgish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lb/
2026-07-28 08:36:53 +00:00
krvi e565073fd5 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-07-28 08:36:53 +00:00
renovate[bot] 2b93191714 Update actions/stale action to v11 2026-07-28 03:41:28 +00:00
Shadowghost 046225654a Queue person metadata refresh instead of blocking the item request and fix ItemCounts 2026-07-27 23:09:30 +02:00
renovate[bot] 0db27d7b1b Update danielpalme/ReportGenerator-GitHub-Action action to v5.5.11 2026-07-27 20:08:23 +00:00
Shadowghost f571cd5a6a Reevaluate pending migrations after each one instead of per stage 2026-07-27 12:35:59 +02:00
Shadowghost 79a55327dc Fix extras naming and version assignment 2026-07-27 12:12:17 +02:00
Cody Robibero dbc796b0b0 Merge pull request #16409 from elio42/fix/create_library_thumbs_on_first_scan
Fix missing collection folder posters after initial scans.
2026-07-26 16:19:25 -04:00
Cody Robibero 02d8e7d828 Merge pull request #17417 from Shadowghost/series-merge-fixes
Fix series merging
2026-07-26 16:19:11 -04:00
Cody Robibero 1e4d126cb9 Merge pull request #17422 from Shadowghost/performance
Reduce correlated subqueries to improve query performance
2026-07-26 16:18:49 -04:00
Shadowghost f3a1d56c56 Use DistinctBy where possible 2026-07-26 09:56:49 +02:00
renovate[bot] 67b8684f3c Update dependency FsCheck.Xunit.v3 to 3.3.4 2026-07-26 00:07:59 +00:00
Shed Shedson b04614e18d Translated using Weblate (Icelandic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/is/
2026-07-25 18:33:24 +00:00
Cody Robibero 689a90d275 Merge pull request #17443 from jellyfin/renovate/sharpcompress-0.x
Update dependency SharpCompress to 0.50.1
2026-07-25 14:05:03 -04:00
Shadowghost 766be1e8bb Fix favorite filter performance 2026-07-25 19:09:39 +02:00
Shadowghost f2f66606d7 Fix DatePlayed sorting performance 2026-07-25 19:09:39 +02:00
renovate[bot] 949fa4b513 Update dependency SharpCompress to 0.50.1 2026-07-25 16:53:47 +00:00
Cody Robibero 40fa2fd390 Merge pull request #17442 from Shadowghost/fix-numbers-in-episode-names
Fix hyphenated numbers in episode titles parsed as multi-episodes
2026-07-25 12:53:38 -04:00
Cody Robibero 7f26bd1091 Merge pull request #17399 from Shadowghost/fix-extra-year
Fix incorrect year on local trailers
2026-07-25 12:52:51 -04:00
Cody Robibero 86ac1aaa6b Merge branch 'master' into fix/create_library_thumbs_on_first_scan 2026-07-25 12:52:11 -04:00
Shadowghost 3c9727d36c Always inherit from owner item and add tests 2026-07-25 17:17:30 +02:00
Shadowghost 5a2809e337 Fix TmdbMissingEpisodeProvider 2026-07-25 16:25:48 +02:00
Shadowghost d1d89dfb12 Fix hyphenated numbers in episode titles parsed as multi-episodes 2026-07-25 16:21:26 +02:00
Cody Robibero 45ec0ed8b5 Merge pull request #17437 from dkanada/ogg-formats
remove ogg from video extensions since it should only be used for audio
2026-07-25 09:17:42 -04:00
Cody Robibero ebb66f6ca3 Merge pull request #17395 from paoloantinori/fix/userdata-null-user-nre-master
Avoid NRE when sorting by user-dependent keys without a user
2026-07-25 08:38:43 -04:00
krvi b85c9186ef Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-07-25 11:40:34 +00:00
Suyash Mittal c606102f6d Translated using Weblate (Hindi)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/
2026-07-25 11:40:33 +00:00
Paolo Antinori 8b70582561 Remove added comments (#17395 review) 2026-07-25 12:50:32 +02:00
dkanada b62fef3c1f remove ogg from video extensions since it should only be used for audio 2026-07-25 14:10:30 +09:00
Cody Robibero 71ab342838 Merge pull request #17423 from Shadowghost/fix-comicinfo
Skip ComicInfo parsing if none exists
2026-07-24 21:43:34 -04:00
Cody Robibero 83c4681e99 Merge pull request #17234 from Eneo-org/fix/syncplay-playqueue-index
Fix play queue index handling in SyncPlay
2026-07-24 21:35:49 -04:00
Cody Robibero 42e52f60ad Merge pull request #17402 from Shadowghost/clean-forced-sort-name
Apply cleaning logic on ForcedSortName
2026-07-24 21:34:37 -04:00
Cody Robibero 6ac64c5319 Merge pull request #17419 from rwebster85/mp4-audio-subtitle-names
Check the "name" tag for audio/subtitle probe to fix MP4 not showing correctly - Fixes issue #17418
2026-07-24 21:30:54 -04:00
Cody Robibero 83e0cfd7ee Merge pull request #17430 from jellyfin/drain-stderr
Drain stderr and stdout concurrently for encoder validation
2026-07-24 21:29:28 -04:00
Cody Robibero 700e53a807 Merge pull request #17411 from mbastian77/perf/session-manager-tolist
perf: avoid unnecessary list allocation in CheckForIdlePlayback
2026-07-24 21:28:55 -04:00
Shadowghost 4d1f90b2f7 Don't pull MusicBrainz Annotations into overview 2026-07-24 19:38:38 +02:00
gnattu d74babd8f3 Drain stderr and stdout concurrently for encoder validation
Some ffmpeg build might output extremely long traces for its banner that consume all pipe capacity and hangs the process. We have to drain both streams regardless on which one we actually read.
2026-07-24 20:01:01 +08:00
Shadowghost b7b2700425 Fix MusicBrainz Metadata fetching 2026-07-23 23:07:58 +02:00
Shadowghost 04e4505402 Fix AudioDB metadata fetching 2026-07-23 22:54:26 +02:00
Shadowghost 95330223f4 Speedup migration 2026-07-23 22:38:45 +02:00
Shadowghost f28fa563c9 Guard against blank names 2026-07-23 22:16:47 +02:00
Shadowghost d6ce6ae8b9 Skip ComicInfo parsing if none exists 2026-07-23 22:08:04 +02:00
Shadowghost 8f57f35372 Implement AudioDb artist search 2026-07-23 21:21:28 +02:00
Shadowghost 9aa79682ba Reduce correlated subqueries to improve performance 2026-07-23 20:50:06 +02:00
Shadowghost d4cddb8a5d Fix series merging 2026-07-23 14:03:13 +02:00
Shadowghost dc300fae53 Allow duplicate LinkedChildren for Playlists 2026-07-23 13:41:51 +02:00
Richard Webster 70980f09de Update contributors 2026-07-23 11:59:43 +01:00
Richard Webster ca0cf763ff Update comment 2026-07-23 11:57:34 +01:00
Richard Webster 3d4c52092e Clarify comment about MP4 track title workaround 2026-07-23 10:13:04 +01:00
mbastian77 fc0af10509 Avoid unnecessary list allocation in CheckForIdlePlayback 2026-07-23 03:25:58 +02:00
renovate[bot] 88216e0ec4 Update github/codeql-action action to v4.37.3 (#17398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 19:46:55 +02:00
Richard Webster 474ae50c36 Check the "name" tag, not just "title" 2026-07-22 18:00:18 +01:00
Shadowghost 929e1936eb Apply cleaning logic on ForcedSortName 2026-07-22 08:30:29 +02:00
Shadowghost 6382563440 Prefer null checks over HasValue everywhere 2026-07-22 08:09:33 +02:00
Shadowghost 733b4ba73a Prefer null checks over HasValue 2026-07-22 08:09:15 +02:00
Paolo Antinori 5d580abb08 fix: avoid NRE when sorting by user-dependent keys without a user
A query sorted by a user-dependent key (PlayCount, IsFavoriteOrLiked,
DatePlayed, IsPlayed, IsUnplayed) but carrying no User caused a
NullReferenceException inside UserDataManager.GetUserData, surfacing as
"Failed to compare two elements in the array" (InvalidOperationException
wrapping the NRE from the LINQ sort) and 500-ing the /Items request.

Root cause: LibraryManager.GetComparer assigned comparer.User = user
without a null guard, so PlayCountComparer.GetValue called
UserDataManager.GetUserData(null, item), dereferencing user.Id.

Two-part fix:
- LibraryManager.GetComparer: when user is null and the sort key requires a
  user (IUserBaseItemComparer), substitute the SortName comparer so the
  result stays deterministic instead of 500-ing. SortName is the project's
  canonical tiebreaker (ItemsController injects it for album-by-artist).
- UserDataManager.GetUserData: ArgumentNullException.ThrowIfNull(user) as
  defense in depth (matches the existing guards on the SaveUserData
  overloads in the same file). On master this overload was rewritten to use
  ResolveUserDataRow, so the NRE dereferences user.Id rather than
  user.InternalId as on the release branch — same bug, different line.

Also fixes DateLastMediaAddedComparer being statically mis-tagged as
IUserBaseItemComparer: its GetDate is static and never reads User, so it
does not need one. Without this, the SortName fallback above would wrongly
engage for DateLastContentAdded on anonymous queries (returning SortName
order instead of date order). Re-tagged to IBaseItemComparer and dropped the
unused User/UserManager/UserDataManager properties.

Tests:
- UserDataManagerTests.GetUserData_NullUser_ThrowsArgumentNullException:
  reproduces the crash (NRE -> now ArgumentNullException). Added to master's
  existing UserDataManagerTests.
- LibraryManagerSortTests.Sort_UserDependentKey_NullUser_FallsBackToSortNameWithoutThrowing:
  Sort with a user-dependent key + null user no longer throws and returns
  items ordered by the SortName fallback (direction preserved).
- LibraryManagerSortTests.Sort_DateLastContentAdded_NullUser_OrdersByDateNotSortName:
  guards that DateLastContentAdded still sorts by date with no user (fixture
  chosen so date-desc and SortName-desc disagree, so a revert is caught).

Full Jellyfin.Server.Implementations.Tests suite: 642 passed, 0 failed.

Fixes #17393
2026-07-22 07:43:58 +02:00
Cody Robibero fc43f151a2 Merge pull request #17227 from altqx/master
Match VobSub MKS subtitle profiles by container
2026-07-21 20:47:02 -04:00
Cody Robibero 526f4051e9 Merge pull request #16980 from TheMelmacian/feature/library_specific_language_filter_values
Improve language filters to only fetch language codes that match the requested items/libraries (follow up to #9787)
2026-07-21 20:43:31 -04:00
Cody Robibero 635fd0433d Merge pull request #17370 from zerafachris/fix/item-update-null-optional-fields
fix: don't throw ArgumentNullException on partial UpdateItem payloads (#17366)
2026-07-21 18:17:22 -04:00
Cody Robibero 370170bab0 Merge pull request #17369 from Shadowghost/harden-startup-wizard
Prevent unauthenticated re-run of the startup wizard on misconfiguration
2026-07-21 18:17:08 -04:00
TheMelmacian b317af0d30 fix: remove obsolete code 2026-07-21 22:20:07 +02:00
Shadowghost ca1f7af445 Fix incorrect year on local trailers 2026-07-21 20:27:04 +02:00
Cody Robibero 65836cc844 Merge pull request #17160 from 854562/truncate-language-strings
Truncate ISO-639-2 language display names at first delimiter
2026-07-21 11:22:32 -04:00
aivarsse b4090bdcb2 Translated using Weblate (Latvian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lv/
2026-07-21 14:35:53 +00:00
Tim Eisele ed61acc19a Fix subtitle encoding for local files (#17281)
* Fix subtitle encoding

* Add short-circuit

* Use IsTextFormat

* Update MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

Co-authored-by: Bond-009 <bond.009@outlook.com>

---------

Co-authored-by: Bond-009 <bond.009@outlook.com>
2026-07-21 14:52:59 +02:00
Bond-009 9b9b609c83 Merge pull request #17368 from Shadowghost/security-path-traversal-fixes
Backport and extend path traversal fixes
2026-07-21 14:52:40 +02:00
Shadowghost 4cc69f4be0 Apply review suggestions 2026-07-21 14:30:55 +02:00
Bond-009 527ba2e11c Merge pull request #17367 from zerafachris/fix/backup-skip-corrupt-keyframe-data
fix: skip corrupt KeyframeData rows during full system backup
2026-07-21 13:53:47 +02:00
Bond-009 25d4e207f7 Merge pull request #17317 from jellyfin/renovate/sharpcompress-0.x
Update dependency SharpCompress to 0.50.0
2026-07-21 13:38:02 +02:00
Bond-009 7a269987d2 Merge pull request #17391 from jellyfin/renovate/ci-deps
Update actions/checkout action to v7.0.1
2026-07-21 13:36:21 +02:00
Bond-009 081944d358 Merge pull request #17376 from mbastian77/docs/model-enums-xml-docs
Add XML docs to small model enums and remove CS1591 suppressions
2026-07-21 13:13:06 +02:00
zerafachris 299810a4a9 fix: use build output directory for backup test temp root to avoid low free-space failures on Windows CI runners
BackupServiceTests rooted its temp directory under Path.GetTempPath(), which
on GitHub-hosted windows-latest runners resolves to the constrained system C:
drive. BackupService.CreateBackupAsync requires 5GiB free at the backup path
before starting, and the C: drive's free temp space can dip below that,
failing CreateBackupAsync_WithCorruptKeyframeDataRow_SkipsRowAndCompletesBackup
even though the fix itself is correct. Rooting the test directory under
AppContext.BaseDirectory keeps it on the same (much larger) drive as the repo
checkout on all platforms, without touching the real BackupService free-space
check.
2026-07-21 08:54:23 +02:00
zerafachris 53e58d8b1b Make ItemUpdateController.UpdateItem internal instead of reflection
Addresses review feedback from @Bond-009 on PR #17370: the test helper
InvokeUpdateItem was invoking the private UpdateItem(BaseItemDto, BaseItem)
method via reflection. Jellyfin.Api.csproj already grants
InternalsVisibleTo("Jellyfin.Api.Tests"), so the method is changed to
internal and the test now calls it directly, removing the
GetMethod/Invoke boilerplate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 08:51:30 +02:00
Shadowghost b99703301f Merge remote-tracking branch 'upstream/master' into security-path-traversal-fixes
# Conflicts:
#	Jellyfin.Api/Controllers/HlsSegmentController.cs
#	Jellyfin.Api/Controllers/PluginsController.cs
2026-07-21 07:14:47 +02:00
Cody Robibero bdf263d867 Merge pull request #17377 from mbastian77/fix/person-visibility-allowed-tags
Exempt people from the allowed tags visibility check
2026-07-20 21:26:06 -04:00
Shed Shedson 9d5aedba5f Translated using Weblate (Icelandic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/is/
2026-07-21 00:52:58 +00:00
Cody Robibero 0f88fe3c7f Merge pull request #17311 from dkanada/book-progress
extract page count from archives and PDFs
2026-07-20 20:50:25 -04:00
Cody Robibero 557b14e33e Merge branch 'master' into fix/backup-skip-corrupt-keyframe-data 2026-07-20 20:49:37 -04:00
Cody Robibero c222d370ce Merge pull request #16933 from WizardOfYendor1/fix/livetv-guide-image-optimization
Feat (fix) - Skip reprocessing program information when importing XMLTV EPG data
2026-07-20 20:17:20 -04:00
Cody Robibero 0d629591ed Remove comments about JSON error handling
Removed comments explaining error handling for malformed JSON during backup.
2026-07-20 20:15:05 -04:00
Cody Robibero 23fa02eb59 Merge pull request #17282 from TowyTowy/fix/13137-clear-profile-image
Fix profile image being impossible to clear when its in-memory key is temporary
2026-07-20 19:58:43 -04:00
Cody Robibero bea016c962 Merge pull request #17320 from TaterTechStudios/fix/item-correct-selector
Fix: Fetch the correct row matching the most up to date file
2026-07-20 19:57:28 -04:00
Cody Robibero fae1e4c556 Merge pull request #17324 from damienmeur/refactor/generic-getorderby
Make RequestHelpers.GetOrderBy generic and reuse it in ActivityLogController
2026-07-20 19:52:10 -04:00
Cody Robibero f2b9c68969 Merge pull request #17342 from LTe/fix-subtitle-conversion-race
Fix race condition in concurrent subtitle conversion
2026-07-20 19:51:44 -04:00
renovate[bot] e04ac6fd35 Update dependency SharpCompress to 0.50.0 2026-07-20 23:46:31 +00:00
renovate[bot] 25011224cf Update actions/checkout action to v7.0.1 2026-07-20 23:46:21 +00:00
Cody Robibero 191be0931e Merge pull request #17254 from sjakub/attribute_aliases
Add additional attribute aliases and improve attribute detection
2026-07-20 19:45:06 -04:00
854562 2cd2f36fe4 Extract truncation logic to helper and add tests 2026-07-20 22:00:10 +02:00
Bond-009 4f19ed5730 Merge pull request #17204 from theguymadmax/sort-seires-trailers
Sort trailers for TV Shows
2026-07-20 12:31:31 +02:00
Bond-009 5949be852a Fix SchedulesDirect image limit recognition (#17347) 2026-07-20 12:28:10 +02:00
theguymadmax ffe075650c Add TVDB provider ID support for movies (#17255)
Add TVDB provider ID support for movies
2026-07-20 12:23:20 +02:00
Bond-009 abb35571ef Merge pull request #17252 from theguymadmax/fix-alubms-and-artists
Fix artists being displayed with albums
2026-07-20 12:20:49 +02:00
Bond-009 474399683d Merge pull request #17386 from jellyfin/renovate/actions-setup-python-7.x
Update actions/setup-python action to v7
2026-07-20 12:16:40 +02:00
gnattu a238d59a07 Remove libpostproc check for ffmpeg version validation (#17384)
Remove libpostproc check for ffmpeg version validation
2026-07-20 12:16:37 +02:00
Bond-009 c779488ade Merge pull request #17382 from kaunkrishna/master
Fix linked whitespace after image badges in `README.md`
2026-07-20 12:15:54 +02:00
krvi cc44f333b6 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-07-20 08:34:15 +00:00
Milo Ivir 2b6302c06e Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/
2026-07-20 08:34:14 +00:00
Bond-009 c3901fde56 Merge pull request #17291 from dkanada/creator-normalization
normalize common formats for creator names in OPF data
2026-07-20 08:39:19 +02:00
Bond-009 aead9c0e22 Merge pull request #17302 from ElianCodes/fix/lastlogindate-stale-entity
Keep authenticated user entity in sync with persisted login timestamps
2026-07-20 08:36:53 +02:00
Bond-009 2fd747432a Merge pull request #17304 from nyanmisaka/normalize-invalid-pts-for-trickplay
Normalize invalid PTS from containers for Trickplay generation
2026-07-20 08:36:25 +02:00
Bond-009 236db6d2f6 Merge pull request #17365 from Shadowghost/fix-resume-perf
Fix Resume query performance
2026-07-20 08:17:38 +02:00
Bond-009 181c057cb8 Merge pull request #17375 from mbastian77/docs/entities-xml-docs
Add XML docs to small entity interfaces and remove CS1591 suppressions
2026-07-20 08:17:04 +02:00
renovate[bot] 7daef156e5 Update actions/setup-python action to v7 2026-07-20 06:12:52 +00:00
KAUN f068ca3bd6 Update README.md 2026-07-19 13:24:22 +05:30
krvi 6c0eff5b39 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-07-18 21:48:35 +00:00
krvi 5e946a45ee Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-07-18 16:27:23 +00:00
TheMelmacian 7d83779b6f fix code style 2026-07-18 17:56:38 +02:00
zerafachris 663a873e07 fix: log corrupt KeyframeData row read failures as errors, not warnings
Per review feedback from cvium: failing to read/backup an entity due to
corrupt underlying data is a significant event that should be surfaced
as an error, not silently downgraded to a warning.
2026-07-18 16:47:23 +02:00
CHO-HSUN-TE 3321a0cf09 Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/
2026-07-18 12:21:45 +00:00
mbastian77 5a9fb80239 Exempt people from the allowed tags visibility check 2026-07-18 14:07:52 +02:00
mbastian77 8ac2d1f7bc Add XML docs to small model enums and remove CS1591 suppressions 2026-07-18 10:08:55 +02:00
nyanmisaka 9fa9d26341 Normalize invalid PTS from containers for Trickplay generation
This change does not affect the keyframe only mode.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-07-18 15:44:34 +08:00
Bond-009 dd0b273b26 Fix Swagger UI auth docs (#12990) (#16910) 2026-07-17 23:48:04 +02:00
Marc Brooks cab108a839 Prevent ffmpeg from hanging extracting subtitles (#17297)
* Prevent ffmpeg from hanging extracting subtitles
Add `RunSubtitleExtractionProcess` to unify the external
_ffmpeg_ process handling and error management.
Add a `-nostdin` flag that prevents _ffmpeg_ from reading from
_stdin_ and blocking on an inherited stdin handle (e.g. when
Jellyfin runs as a service under NSSM), which otherwise hangs
subtitle extraction forever when _ffmpeg_ blocks on any
keyboard-interaction read until the timeout (30 minutes).
Close the redirected _stdin_ to ensure immediage EOF.
Drain the _stderr_ to a string and log it, to ensure we don't block
the _ffmpeg_ process on errors that exceed the pipe length.
Pass `-y` to _ffmpeg_ to ensure it overwrites any existing output file
without prompting for confirmation.

* Address review comments
Make sure we always drain stderr.
Make sure the timeout also honors the cancellationToken.
Make sure when we get cancelled we don't log it as a ffmpeg error.
2026-07-17 23:23:57 +02:00
Rant423 3d83e67a52 Show production companies under TV Shows' Studios (#17246)
* show production companies instead of networks

* keep both production companies and networks

* fix whitespace

* fix nullable type

* networks first, then production companies
2026-07-17 22:45:54 +02:00
Bond-009 b809d964b3 Merge pull request #17288 from nyanmisaka/ffmpeg-log-utf8
Fix potential garbled text in FFmpeg logs on Windows
2026-07-17 22:28:49 +02:00
Bond-009 96727072c8 Merge pull request #17327 from Shadowghost/remove-playbackpositionticks-mediasourceinfo
Remove PlaybackPositionTicks from MediaSourceInfo
2026-07-17 22:21:21 +02:00
Bond-009 9ec0d7c63b Merge pull request #17334 from nyanmisaka/fix-cuda-hwupload
Fix format negotiation in hybrid SW decode and CUDA tonemap pipeline
2026-07-17 22:15:49 +02:00
Bond-009 bfe0a47b32 Merge pull request #17151 from theguymadmax/fix-indentify-image
Fix Identify returning wrong images
2026-07-17 22:13:47 +02:00
Bond-009 9b5f830462 Merge pull request #17326 from theguymadmax/update-series-name
Update season and episode SeriesName when renaming a series
2026-07-17 22:06:26 +02:00
Bond-009 42f9ed76c0 Merge pull request #17280 from Shadowghost/remove-image-override-hack
Remove episode image override hack
2026-07-17 22:06:07 +02:00
Bond-009 69faa6c583 Merge pull request #17191 from IDisposable/fix/handler-path-traversal
Fix path transversal exposure in Plugins
2026-07-17 21:52:22 +02:00
WizardOfYendor1 0c7428f136 Added verbose, rambling, log warning to help users with config issues (hoping to reduce false issues reports).
Also added a test to exercise it, which is perhaps silly but convenient.
2026-07-17 14:59:46 -04:00
WizardOfYendor1 97e666c566 Append base URL if the published server URL override omits it. Fleshed out unit tests to cover that and https->http reverse proxy scenario(s). 2026-07-17 14:59:46 -04:00
zerafachris 5cd3d7ebb7 fix: don't throw ArgumentNullException on partial UpdateItem payloads (#17366)
BaseItemDto.Genres, .Tags, and .ProviderIds are plain auto-properties with
no default initializer, so they deserialize to null when a client omits
them from a partial POST /Items/{itemId} body. The OpenAPI spec documents
every BaseItemDto field as optional, but ItemUpdateController.UpdateItem
fed these three properties straight into Distinct()/Select()/ToList()
without a null check, so a request that (for example) only sets Tags
throws ArgumentNullException("source") once it reaches the unguarded
Genres line, before Tags is even processed.

Guard all three assignments with the same "if (request.X is not null)"
pattern already used for the neighboring Studios/Taglines/ProductionLocations
fields in this method, so omitted fields are left unchanged instead of
crashing the request.

Adds ItemUpdateControllerTests covering the reported repro (only Tags
supplied) and a companion case asserting existing Genres/ProviderIds are
preserved when omitted from the payload.

Signed-off-by: zerafachris <christopher.zerafa@blocklabs.io>
2026-07-17 17:20:30 +02:00
Shadowghost 62a5ded920 Prevent unauthenticated re-run of the startup wizard on misconfiguration 2026-07-17 17:14:27 +02:00
Shadowghost 21801e8ba1 Harden remaining path-construction sinks against traversal 2026-07-17 17:08:59 +02:00
Shadowghost 1a45fc82b5 Sanitize media attachment and lyric paths against traversal 2026-07-17 17:07:27 +02:00
Shadowghost 4fb779920a Sanitize ClientLog upload filename to prevent path traversal 2026-07-17 17:02:02 +02:00
zerafachris d7727224c2 Skip corrupt KeyframeData rows during full system backup
A single row with malformed KeyframeTicks JSON (e.g. a truncated array
from an interrupted write) currently aborts the entire backup, because
the try/catch in BackupService.CreateBackupAsync only wraps
serialization of an already-materialized entity, not the enumeration
itself. EF Core throws JsonReaderException from MoveNextAsync() while
materializing the corrupt row, which propagates past that catch block.

Switch to manual enumerator iteration so MoveNextAsync() failures can
be caught per-row, logged as a warning identifying the affected table,
and skipped, allowing the remaining rows and the rest of the backup to
complete.

Fixes #17216

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 16:44:25 +02:00
Bond-009 a96dc8bd9b Merge pull request #17354 from jellyfin/renovate/actions-setup-dotnet-6.x 2026-07-17 13:56:55 +02:00
Bond-009 d146c347b4 Merge pull request #17359 from jellyfin/renovate/ci-deps 2026-07-17 13:54:14 +02:00
Bond-009 f2dea2a3fd Merge pull request #17348 from theguymadmax/revert-includeItemTypes-in-collectionType 2026-07-17 13:53:58 +02:00
Bond-009 4d1dd42420 Merge pull request #17343 from mbastian77/docs/channels-xml-docs 2026-07-17 13:53:27 +02:00
Bond-009 8eb0ba60c1 Merge pull request #17344 from mbastian77/docs/session-model-xml-docs 2026-07-17 13:52:59 +02:00
Bond-009 2bf15d95df Merge pull request #17340 from mbastian77/docs/dlna-model-xml-docs 2026-07-17 13:52:31 +02:00
Bond-009 be2db99586 Merge pull request #17339 from mbastian77/docs/providers-lookup-info-xml-docs 2026-07-17 13:51:59 +02:00
Bond-009 37fe9df27c Merge pull request #17338 from mbastian77/docs/deviceid-xml-docs 2026-07-17 13:51:29 +02:00
Fabián Sanhueza 8759ad4d49 Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_419/
2026-07-17 11:49:50 +00:00
Bond-009 051ff4cf6d Merge pull request #17337 from Shadowghost/limit-similar-items 2026-07-17 13:49:46 +02:00
theguymadmax fa4626c080 Revert setting default BaseItemKind for CollectionType 2026-07-16 18:15:54 -04:00
Enea D'Angiò d36c8ebce8 Reduce cognitive complexity of RemoveFromPlaylist 2026-07-16 21:03:29 +02:00
renovate[bot] 4cc577a72d Update github/codeql-action action to v4.37.1 2026-07-16 18:47:00 +00:00
Shadowghost cdf7ce0fc4 Fix user data batch query perf 2026-07-16 14:21:03 +02:00
Shadowghost d894a98b79 Fix tie-breaker performance 2026-07-16 14:20:42 +02:00
renovate[bot] 2b57a5725c Update actions/setup-dotnet action to v6 2026-07-16 07:02:47 +00:00
Jordan Rushing 3f96790904 Move GetUserDataBatch to use ResolveUserDataRow when item.UserData isn't preloaded 2026-07-15 16:21:42 -05:00
Shadowghost 2bac9a8f0c Fix SchedulesDirect image limit recognition 2026-07-15 17:43:27 +02:00
mbastian77 997093ae3a Add XML docs to small entity interfaces and remove CS1591 suppressions 2026-07-15 16:07:56 +02:00
mbastian77 5cbafa566d Add XML docs to small session model types and remove CS1591 suppressions 2026-07-15 16:03:56 +02:00
mbastian77 0d3cf0169e Add XML docs to small channel types and remove CS1591 suppressions 2026-07-15 16:02:12 +02:00
Piotr Niełacny 6e3c187493 Fix race condition in concurrent subtitle conversion
SubtitleEncoder.ConvertSubtitles parsed subtitles with libse's static
Subtitle.Parse, which iterates a statically cached list of shared
SubtitleFormat instances. Format parsers keep mutable per-parse state on
the instance, so concurrent subtitle requests corrupted each other's
output (cues mixed across streams and languages, truncated files) or
failed with NullReferenceException when format detection broke down and
Subtitle.Parse returned null.

Parse through the injected ISubtitleParser instead. SubtitleEditParser
instantiates a fresh format parser per call, so requests no longer share
state. Its Parse method now returns the libse Subtitle directly (the
SubtitleTrackInfo flattening was unused since the SubtitleEdit writer
rework) so the writers keep full fidelity such as ASS styling.
2026-07-15 14:55:55 +02:00
Bond-009 f8771b52ec Merge pull request #17331 from jellyfin/renovate/dotnet-monorepo 2026-07-15 12:49:34 +02:00
Bond-009 e3d610c3c5 Merge pull request #17274 from theguymadmax/fix-max-login-attempts 2026-07-15 12:42:09 +02:00
Bond-009 486ffaa6dd Merge pull request #17248 from theguymadmax/add-novel 2026-07-15 12:40:17 +02:00
mbastian77 d96d1f1119 Add XML docs to small DLNA model types and remove CS1591 suppressions 2026-07-15 12:11:11 +02:00
mbastian77 30f28456de Add XML docs to lookup info types and remove CS1591 suppressions 2026-07-15 12:08:08 +02:00
mbastian77 f8aad322cc Add XML docs to DeviceId and remove CS1591 suppression 2026-07-15 11:58:29 +02:00
Shadowghost c6545a8b68 Limit similar items to user accessible libraries 2026-07-15 11:47:59 +02:00
Bond-009 0fe9c1ce91 Merge pull request #17330 from jellyfin/renovate/microsoft 2026-07-15 11:42:52 +02:00
Bond-009 b6882c86dc Merge pull request #17250 from TaterTechStudios/feature/books-series-name-metadata 2026-07-15 07:57:57 +02:00
nyanmisaka 4503ad295c Fix format negotiation in hybrid SW decode and CUDA tonemap pipeline
The CUDA hwcontext in FFmpeg 8.1 has added support for 10bit
fully-planar formats, but few CUDA filters support them.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-07-15 13:54:45 +08:00
renovate[bot] 1a9bd2417a Update Microsoft 2026-07-14 20:11:54 +00:00
renovate[bot] 0d2f1b26b3 Update dependency dotnet-ef to v10.0.10 2026-07-14 20:11:43 +00:00
Bond-009 bc59e56ef5 Merge pull request #17266 from Shadowghost/fix-non-admin-additional-parts 2026-07-14 15:36:38 +02:00
Shadowghost b8bac71270 remove PlaybackPositionTicks from MediaSourceInfo 2026-07-14 10:05:32 +02:00
theguymadmax 6238448716 Update season and episode SeriesName when renaming a series 2026-07-13 23:08:06 -04:00
Damien Meur 56c970c2db Make RequestHelpers.GetOrderBy generic and reuse it in ActivityLogController 2026-07-14 02:57:15 +02:00
Jordan Rushing fcce108948 Fix: Fetch the correct row matching the most up to date file 2026-07-13 15:40:40 -05:00
Bond-009 cfeaef6180 Merge pull request #17306 from theguymadmax/fix-strm-sub-protocol 2026-07-13 20:14:20 +02:00
Bond-009 ebab369eac Merge pull request #17273 from theguymadmax/ratigs-numerical-score 2026-07-13 20:13:29 +02:00
TheMelmacian 2a44c35224 Apply review suggestions 2026-07-13 20:01:01 +02:00
DrummingBird1 0584a102ee Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/
2026-07-13 11:26:23 +00:00
theguymadmax 2aae53bc15 Apply review feedback 2026-07-12 11:54:56 -04:00
Bond-009 911044b415 Merge pull request #17287 from dkanada/audiobook-cover 2026-07-12 17:39:34 +02:00
Bond-009 b77def6883 Merge pull request #17268 from theguymadmax/fix-greece-ratings 2026-07-12 17:36:01 +02:00
Bond-009 5c48935fa8 Merge pull request #17220 from jellyfin/renovate/ci-deps 2026-07-12 17:33:36 +02:00
dkanada 9e996d612c extract page count from archives and PDFs 2026-07-12 19:46:56 +09:00
TowyTowy 5be844e1b7 Fix 3D format detection when the tag is the last token of the path
Format3DParser drops the last character of the final path token: when
IndexOfAny finds no more delimiters, the slice is taken with
'index = path.Length - 1', so e.g. "hsbs" is compared as "hsb" and
never matches any rule.

File paths are unaffected because the extension is always the final
token, but directory based media have no extension. For DVD/BluRay
folder rips (BaseVideoResolver parses the folder path via
Set3DFormat), a trailing 3D tag such as
"Gravity (2013) 3d hsbs/BDMV" is silently ignored and Video3DFormat
is never set.

This is a regression from 42a2cc174 which replaced the string.Split
based FlagParser with span slicing; the Split implementation kept the
final token intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:11:29 +02:00
theguymadmax e2d9d592bc Fix incorrect protocol used for subtitle charset detection 2026-07-11 17:33:06 -04:00
WizardOfYendor1 6f189bf2b8 Reduce GetPlaybackInfo cognitive complexity 2026-07-11 10:12:42 -04:00
Elian Van Cutsem 47f567b6f1 Keep authenticated user entity in sync with persisted login timestamps
ExecuteUpdateAsync bypasses the EF change tracker, so the user entity
returned by AuthenticateUser still carried the old LastLoginDate and
LastActivityDate. SessionManager.LogSessionActivity then saved that
stale entity in full, reverting LastLoginDate (usually to null)
milliseconds after every login. Setting the properties on the entity
keeps the follow-up save consistent and lets the 60-second activity
guard skip the redundant write during login.

Fixes #17301
2026-07-11 14:04:07 +02:00
dkanada 305160d2e4 enable audiobook image saving to local folder 2026-07-11 11:00:46 +09:00
WizardOfYendor1 f3ff7a446b Add WizardOfYendor1 to contributors 2026-07-10 20:31:14 -04:00
WizardOfYendor1 a3ec1a3712 Add Live TV published URL regression coverage 2026-07-10 20:27:10 -04:00
WizardOfYendor1 6bbd6dcd44 Resolve Live TV client stream URLs per request 2026-07-10 20:26:29 -04:00
WizardOfYendor1 ce43df6f43 Fix host and port handling for published server URI overrides 2026-07-10 14:19:08 -04:00
dkanada 2250015e78 normalize common formats for creator names in OPF data 2026-07-10 22:53:14 +09:00
renovate[bot] 933f7eea1b Update CI dependencies 2026-07-10 07:47:01 +00:00
nyanmisaka 631a314d24 Fix potential garbled text in FFmpeg logs on Windows
Explicitly set StandardErrorEncoding and StandardOutputEncoding to
Encoding.UTF8 when invoking the FFmpeg subprocess.

This prevents log encoding issues and character corruption on Windows
environments that default to non-UTF8 ANSI code pages.

This fixes garbled metadata and font names in the FFmpeg logs.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-07-10 14:46:27 +08:00
dkanada ed06fd5139 support external images for audiobooks 2026-07-10 14:19:45 +09:00
TowyTowy 2326ecdedc Fix profile image being impossible to clear when its in-memory key is temporary
ClearProfileImageAsync removed the ProfileImage instance attached to the
passed-in User, but that instance can carry a stale, never-persisted
(temporary) key because UpdateUserAsync creates the persisted image on a
separately loaded entity and never copies the generated key back.
Removing that detached entity on a fresh DbContext made EF Core throw
InvalidOperationException ('ImageInfo.Id has a temporary value'), leaving
the profile image impossible to delete or replace.

Load the tracked, persisted user and remove its actual ProfileImage,
matching the removal pattern already used in UpdateUserAsync. Adds
regression tests covering the temporary-key case and the no-image no-op
(the first fails before this change and passes after).

Fixes #13137

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:39:09 +02:00
Shadowghost 38813f7d42 Cleanup PreferEpisodeParentPoster) 2026-07-09 12:07:13 +02:00
Shadowghost 853922443f Remove episode image override hack 2026-07-09 11:54:18 +02:00
theguymadmax 8b826d981b Fix max login attempts 2026-07-08 23:07:57 -04:00
theguymadmax 8bf1710e07 Check numeric rating value after splitting country code 2026-07-08 20:09:49 -04:00
theguymadmax 5e42941d3b Fix Greece parental ratings 2026-07-08 12:09:38 -04:00
theguymadmax a1255bda6a Use FrozenSet 2026-07-08 11:45:16 -04:00
Shadowghost 9a2fdb3573 Fix additional parts for non-admins 2026-07-08 13:43:30 +02:00
Jakub Schmidtke 08f6627a24 Replaced string.Empty with ReadOnlySpan<char>.Empty 2026-07-08 01:40:26 +02:00
Shed Shedson 53aafcd38e Translated using Weblate (Icelandic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/is/
2026-07-07 18:12:36 +00:00
Shed Shedson d73e65172a Translated using Weblate (Icelandic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/is/
2026-07-07 16:33:35 +00:00
theguymadmax fff710585f Restore music collection image override 2026-07-07 10:24:18 -04:00
Jakub Schmidtke 1294990f4f Added more aliases for attributes
Adds tvdb alias for tvdbid and imdb alias for imdbid.

It also fixes an issue where tmdb alias was being ignored
if it was followed by something like "tmdbidfoo".
The same issue prevented imdb pattern matching from
working, if it was followed by something like "imdbidfoo".

It also allows for detecting the first matching occurence,
whether it was an alias or not.

Finally, it ignores attributes with values consisting of only whitespaces.
2026-07-07 15:39:09 +02:00
yuuta0331 974038e9b0 Translated using Weblate (Japanese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/
2026-07-07 02:20:57 +00:00
theguymadmax ab0d0d1890 Fix artists being displayed with albums 2026-07-06 22:06:34 -04:00
Jordan Rushing f3fbe5575a Allow SeriesName to be editable from Item Metadata (books) 2026-07-06 15:28:49 -05:00
theguymadmax 860cb75d58 Add Novel job mapping to the Writing department 2026-07-06 11:27:24 -04:00
Lofuuzi e31f168e9b 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-07-06 08:24:25 +00:00
Ulrik 7f5537ca47 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/
2026-07-06 05:12:54 +00:00
Cody Robibero a03072b617 Merge pull request #16668 from johnpc/fix/people-images-not-displayed
Fix actor images not displayed until clicked
2026-07-05 16:25:38 -04:00
Cody Robibero 31d05dbdd6 Merge pull request #17228 from Shadowghost/fix-logout-concurrency
Don't throw on logout if session does not exist
2026-07-05 16:22:45 -04:00
Cody Robibero d4faf7bd72 Merge pull request #17238 from iderex/fix/dateparse-format-provider
Use InvariantCulture when parsing machine-generated dates
2026-07-05 16:21:56 -04:00
Cody Robibero 6e728b009f Merge pull request #17044 from Shadowghost/version-model-and-handling
Fixes for multi version handling
2026-07-05 16:21:02 -04:00
Cody Robibero efd3814a7e Merge pull request #17239 from theguymadmax/ratings-separator
Fix parental rating lookup for multi-rating entries
2026-07-05 16:20:45 -04:00
Bond-009 b391cd5e9e Merge pull request #17231 from theguymadmax/fix-paths-not-being-deleted
Fix ghost entries when deleting library paths
2026-07-05 16:19:46 +02:00
Bond-009 8433773fad Allow changing capitalization of usernames (#17229)
Fixes #17195
Adds a regression test
2026-07-05 15:59:14 +02:00
Lofuuzi 381bf18161 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-07-05 08:08:46 +00:00
Nils Lehnen f8ffccae7f Use InvariantCulture when parsing machine-generated dates
DateTime.TryParse without an IFormatProvider falls back to the current
thread culture, so the same string can parse differently (or fail)
depending on the server's locale. None of these call sites deal with
user-entered text - they parse dates that come from filenames, an
HTTP header, ffprobe metadata and values the app itself wrote to the
auth database - so InvariantCulture is the correct provider everywhere
here.

Fixes the S6580 / CA1305 warnings on these call sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:55:31 +02:00
theguymadmax 2528bc1032 Fix parental rating lookup for multi-rating entries 2026-07-04 17:53:55 -04:00
Ulrik 4b5f5d6ca3 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/
2026-07-04 21:25:02 +00:00
Cody Robibero e2eaacd239 Merge pull request #17222 from theguymadmax/fix-folder-view
Fix folder view
2026-07-04 10:06:44 -04:00
Cody Robibero 77d4ab9ea9 Merge pull request #17225 from jellyfin/renovate/microsoft
Update Microsoft to 5.6.0
2026-07-04 10:06:11 -04:00
Enea D'Angiò 6883cd0969 Fix play queue index handling in SyncPlay
Three index bugs in PlayQueueManager, two of which leave
PlayingItemIndex out of bounds, making every subsequent Buffering/Ready
request throw and leaving the group unusable until it empties:

- RemoveFromPlaylist did not compensate for removed items preceding the
  playing item: removing the playing item together with earlier items
  could select the wrong item or crash with an out-of-bounds index.
- Next/Previous on an empty playlist with RepeatOne/RepeatAll reported
  success or set PlayingItemIndex to 0 on an empty list, crashing
  downstream in Group and corrupting the index.
- SetPlayingItemByIndex accepted an index equal to the playlist count
  (latent off-by-one, callers currently pre-validate).
2026-07-04 11:38:05 +02:00
theguymadmax 43a152359e Fix ghost entries when deleting library paths 2026-07-03 14:12:56 -04:00
Bond_009 482cf4b8c3 Allow changing capitalization of usernames
Fixes #17195
Adds a regression test
2026-07-03 18:33:10 +02:00
Shadowghost 2b4945217a Don't throw on logout if session does not exist 2026-07-03 15:58:57 +02:00
Chamithu Mapalagama ccc1712d10 Translated using Weblate (Sinhala)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/si/
2026-07-03 03:37:35 +00:00
altqx 8622c3bfb7 Match VobSub MKS subtitle profiles by container 2026-07-03 09:30:22 +07:00
renovate[bot] 820e7b91da Update Microsoft to 5.6.0 2026-07-02 23:57:21 +00:00
theguymadmax 08ba3717ea Fix folder view 2026-07-02 14:04:20 -04:00
Enea D'Angiò 8f3eb3205d Close sessions for lost WebSockets to prevent zombie SyncPlay groups (#17079)
Close sessions for lost WebSockets to prevent zombie SyncPlay groups
2026-07-02 19:36:48 +02:00
Bond-009 379c58a48d Merge pull request #17209 from theguymadmax/update-swedish-ratings
Fix Swedish rating
2026-07-02 19:29:12 +02:00
Bond-009 28b43838dd Merge pull request #17206 from zachhide/fix/livetv-hls-null-mediasource
Fix NullReferenceException in GetStreamingState for closed live streams
2026-07-02 19:29:06 +02:00
Shadowghost 38f1d9749e Fix review comments 2026-07-02 08:49:11 +02:00
m4st3r-0day 9cc25d133d Translated using Weblate (Albanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sq/
2026-07-01 20:34:37 +00:00
theguymadmax 8e1b69b37f Add missing Swedish ratings 2026-06-30 16:34:05 -04:00
Bond-009 a43fd188dc Merge pull request #17175 from obrenoalvim/fix/use-leftjoin-ef10
Use Enumerable.LeftJoin for activity log user query
2026-06-30 17:49:15 +02:00
Breno Alvim f011529388 Use Enumerable.LeftJoin for activity log user query 2026-06-30 17:37:02 +02:00
zachhide 8ffb54603a Fix NullReferenceException in GetStreamingState for closed live streams
When a client polls the HLS playlist (e.g. live.m3u8) after a live stream has
been disposed because its consumer count dropped to zero,
GetLiveStreamWithDirectStreamProvider returns a null MediaSource. The live
branch of GetStreamingState then dereferenced it unconditionally, throwing a
NullReferenceException and returning HTTP 500 for every poll until the client
re-opens the stream. Guard against the null MediaSource and throw
ResourceNotFoundException so the request returns 404 instead of crashing.

Fixes #17009

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 01:13:51 -04:00
Bond-009 d3ee1e84b1 Merge pull request #17170 from Shadowghost/better-bitrates
Rework bitrate reporting
2026-06-29 18:06:14 +02:00
Bond-009 1035f6a101 Merge pull request #15954 from IDisposable/fix/books
Fix Book collections speed issues
2026-06-29 18:05:55 +02:00
theguymadmax 22792b62cb Sort trailers for TV Shows 2026-06-29 12:04:55 -04:00
John Corser ef6f342a54 Use IItemTypeLookup and QueryPartitionHelpers
Address review feedback:
- Replace typeof(Person).FullName with IItemTypeLookup.BaseItemKindNames
- Replace foreach+ToListAsync with PartitionEagerAsync for batched
  iteration with built-in progress reporting
- Check HasImage/HasOverview on the loaded domain Person object
  instead of projecting from the DB query
2026-06-28 21:44:47 -04:00
John Corser a888257c82 Project hasImage/hasOverview from DB query
Instead of re-checking image/overview on the domain object after loading,
project the values directly from the database query as part of the
anonymous type selection. This avoids redundant checks since the DB
already has this information.
2026-06-28 21:44:46 -04:00
John Corser d55f808423 Move people filtering to database query
Instead of loading all people names and checking each one in memory,
query the database directly for Person items that need refresh:
- Missing primary image OR missing overview
- Not refreshed within the last 30 days

This reduces the operation from N+1 queries (1 for all names + 1 per
person to load) to a single filtered query returning only the IDs that
need work.
2026-06-28 21:44:46 -04:00
John Corser dc92e3b0e4 Fix actor images not displayed until clicked
Move image refresh logic from PeopleValidator (which runs during library
scans) into PeopleValidationTask (the "Refresh People" scheduled task).
This keeps library scans fast while ensuring the scheduled task fetches
missing images from remote providers like TMDB.

People missing a Primary image or overview get refreshed with
MetadataRefreshMode.Default instead of ValidationOnly, with a 30-day
cooldown to avoid hammering providers for people they have no data for.

Fixes jellyfin#8103
2026-06-28 21:44:46 -04:00
Marc Brooks 95cebffa87 Add tests
Also fixed a sibling directory that matches the prefix.
2026-06-28 16:26:35 -05:00
Marc Brooks 617ebf367f Fix path transversal exposure in Plugins
The request path is not validated to a valid path and could allow escaping the transcode path and downloading of any arbitrary file in GetHlsPlaylistLegacy .

GetHlsAudioSegmentLegacy and GetHlsVideoSegmentLegacy have the same issue, and are NOT behind an Authorize so they are publicly exploitable.

Added a ValidateTranscodePath that verifies that requested file paths start with the transcode path setting. Also ensure that all filename comparisons are OrdinalIgnoreCase because we might be running on a filesystem where filename-casing doesn't have to match. Switched from InvariantCulture because the underlying OS filename comparisons are always byte-wise (with case insensitivity here).

Fixed a similar issue in GetPluginImage
2026-06-26 12:05:54 -05:00
Marc Brooks 70b4589382 Fix Book collections scanning all items
Added static method GetBaseItemKindsForCollectionType in ItemsController (moved from ContentFolderImageProvider to be shared)

Added AudioBook to GetRepresentativeItemTypes for CollectionType.books for consistency

Added GetBooks to GetUserItems for CollectionType.books which gets BaseItemKind.Book and BaseItemKind.AudioBook

Move GetBaseItemKindsForCollectionType to DtoExtensions

Cleaned up the missing null checks and used new collection expressions.
Associate Person to Book and AudioBook for related items.
2026-06-26 11:25:58 -05:00
Shadowghost 5221584b06 Only process items with tmdb id 2026-06-25 20:54:05 +02:00
Shadowghost c4ace4ac95 Set TMDb id in season provider 2026-06-25 19:29:52 +02:00
Shadowghost 2fcf4084f8 Add TMDb missing episode provider 2026-06-25 19:10:38 +02:00
854562 c632417dda Fix SonarCloud warnings 2026-06-23 18:32:47 +02:00
Shadowghost d090c59939 Rework bitrate reporting 2026-06-23 17:47:17 +02:00
854562 94d5326411 Truncate ISO-639-2 language display names at first delimiter
Prevents raw ISO-639-2 values (e.g. "Greek, Modern (1453-)" from cluttering the audio and subtitle display names by truncating them at the first comma or semicolon ("Greek"). Applies to MediaStreamRepository and ProbeResultNormalizer.
2026-06-22 20:25:13 +02:00
theguymadmax c49f279a27 Fix Identify returning wrong images 2026-06-21 18:22:00 -04:00
Shadowghost 0fb042b740 Surface the played version for resume 2026-06-19 21:51:57 +02:00
Shadowghost a5706c2fa6 Reconcile Seasons and Episodes 2026-06-12 21:20:53 +02:00
Shadowghost 4278b34fe6 Merge remote-tracking branch 'upstream/master' into version-model-and-handling 2026-06-12 08:35:39 +02:00
Shadowghost 0aeee8233b Fix performance 2026-06-12 08:35:30 +02:00
Shadowghost 95de28cdda Merge remote-tracking branch 'upstream/master' into version-model-and-handling 2026-06-10 08:05:03 +02:00
Shadowghost 0874a26131 Coalesce alternate-version progress onto primary in resume filter 2026-06-09 23:23:03 +02:00
Elio Kuster 1bfbad2420 Fix missing collection folder posters after initial scans.
Fixes #1200
2026-06-09 20:34:24 +02:00
Shadowghost fe1d8d8840 Collapse version groups to the primary version in queries 2026-06-07 23:06:14 +02:00
Shadowghost a53a533cc4 Respect user permissions in version count 2026-06-07 23:06:14 +02:00
Shadowghost 4725722b1c Make ContinueWatching and NextUp version-aware 2026-06-07 23:06:14 +02:00
Shadowghost 9ea3f45886 Make resume queries version-aware 2026-06-07 23:06:14 +02:00
Shadowghost e64cc73f88 Mark only linked alternate versions as grouped media sources 2026-06-07 23:06:14 +02:00
Shadowghost 63990d6a2d Surface extras across all versions 2026-06-07 23:06:14 +02:00
Shadowghost 09723bd123 Aggregate alternate versions via GetAllVersions 2026-06-07 23:06:14 +02:00
Shadowghost c242533f4e Add version-aware playback tracking 2026-06-07 23:06:14 +02:00
Shadowghost 507998a4e3 Derive version-aware media source names 2026-06-07 23:06:14 +02:00
TheMelmacian 7939f3b009 only fetch language codes for the requested library when generating filter values 2026-05-30 16:33:59 +02:00
WizardOfYendor1 e1d63c0ea0 Fixed issue etag info was not being set until the 3rd time though processing due to the "isNew" condition. Etag wouldn't be saved/persistent until the 3rd time through and onward.
Without this change it's self healing after the 3rd cycle.

It also appears there may be an issue with this etag "skip if hash hasn't changed" for schedules direct functionality.... like it never will work. But out of scope here.

Also fixed Sonar gripes about code formatting
2026-05-25 20:04:45 -04:00
WizardOfYendor1 e1a16b4ec6 Add XMLTV guide content ETags 2026-05-25 20:04:21 -04:00
chasus 9569b4550d Fix Swagger UI auth docs (#12990) 2026-05-24 00:04:10 +10:00
372 changed files with 21728 additions and 2484 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.9",
"version": "10.0.10",
"commands": [
"dotnet-ef"
]
+5 -5
View File
@@ -24,21 +24,21 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
+4 -4
View File
@@ -11,13 +11,13 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'
@@ -40,14 +40,14 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'
+2 -2
View File
@@ -15,9 +15,9 @@ jobs:
format-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
+3 -3
View File
@@ -20,9 +20,9 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
@@ -35,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10
uses: danielpalme/ReportGenerator-GitHub-Action@d3ebf1f760f7d8ab92cc44d9bcfee7ad73722a31 # v5.5.11
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
+3 -3
View File
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -40,12 +40,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: pull in script
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
cache: 'pip'
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
+2 -2
View File
@@ -10,12 +10,12 @@ jobs:
issues: write
steps:
- name: pull in script
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
cache: 'pip'
+2 -2
View File
@@ -22,13 +22,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
repository: ${{ inputs.repository }}
- name: Configure .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
base_ref: ${{ steps.ancestor.outputs.base_ref }}
steps:
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
+2 -2
View File
@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.TAG_BRANCH }}
+4
View File
@@ -172,6 +172,7 @@
- [whooo](https://github.com/whooo)
- [WiiPlayer2](https://github.com/WiiPlayer2)
- [WillWill56](https://github.com/WillWill56)
- [WizardOfYendor1](https://github.com/WizardOfYendor1)
- [wtayl0r](https://github.com/wtayl0r)
- [Wuerfelbecher](https://github.com/Wuerfelbecher)
- [Wunax](https://github.com/Wunax)
@@ -233,6 +234,9 @@
- [MSalman5230](https://github.com/MSalman5230)
- [dwandw](https://github.com/dwandw)
- [Lampan-git](https://github.com/Lampan-git)
- [elio42](https://github.com/elio42)
- [rwebster85](https://github.com/rwebster85)
- [Florin-Popescu](https://github.com/Florin-Popescu)
# Emby Contributors
+26 -25
View File
@@ -17,7 +17,7 @@
<PackageVersion Include="Diacritics" Version="4.1.8" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.3" />
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.4" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.5" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
@@ -26,33 +26,34 @@
<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.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<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="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="1.1.0.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="PDFtoImage" Version="5.2.1" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
@@ -67,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.49.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" />
@@ -77,7 +78,7 @@
<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.9" />
<PackageVersion Include="System.Text.Json" Version="10.0.10" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.15.3" />
<PackageVersion Include="TMDbLib" Version="3.0.0" />
+4 -2
View File
@@ -57,7 +57,6 @@ namespace Emby.Naming.Common
".nrg",
".nsv",
".nuv",
".ogg",
".ogm",
".ogv",
".pva",
@@ -362,7 +361,10 @@ namespace Emby.Naming.Common
// Not a Kodi rule as well, but the expression below also causes false positives,
// so we make sure this one gets tested first.
// "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
// Names carrying an SxxEyy marker are excluded because the Kodi expression above already covers them.
// Without that guard this expression reads digits out of the title instead, turning
// "S01E01 1-23-45 [Bluray-1080p]" into episodes 1 through 45.
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?![^\\\/]*[Ss][0-9]+[][ ._-]*[Ee][0-9]+)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
{
IsNamed = true
},
+1 -1
View File
@@ -125,7 +125,7 @@ namespace Emby.Naming.TV
result.Success = true;
}
}
else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date))
else if (DateTime.TryParse(match.Groups[0].ValueSpan, CultureInfo.InvariantCulture, out date))
{
result.Year = date.Year;
result.Month = date.Month;
+9 -4
View File
@@ -52,13 +52,18 @@ namespace Emby.Naming.Video
while (path.Length > 0)
{
var index = path.IndexOfAny(delimiters);
ReadOnlySpan<char> currentSlice;
if (index == -1)
{
index = path.Length - 1;
// No delimiter left, the last token is the remainder of the path
currentSlice = path;
path = default;
}
else
{
currentSlice = path[..index];
path = path[(index + 1)..];
}
var currentSlice = path[..index];
path = path[(index + 1)..];
if (!foundPrefix)
{
+25 -2
View File
@@ -39,6 +39,8 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Data;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
@@ -417,6 +419,8 @@ namespace Emby.Server.Implementations
{
Logger.LogInformation("Running startup tasks");
EnsureStartupWizardIntegrity();
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
@@ -436,6 +440,24 @@ namespace Emby.Server.Implementations
return Task.CompletedTask;
}
private void EnsureStartupWizardIntegrity()
{
if (ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
return;
}
var hasConfiguredAdministrator = Resolve<IUserManager>().GetUsers()
.Any(user => user.HasPermission(PermissionKind.IsAdministrator) && !string.IsNullOrEmpty(user.Password));
if (hasConfiguredAdministrator)
{
Logger.LogWarning("The startup wizard is marked incomplete but a configured administrator already exists. Marking setup as completed to prevent the unauthenticated setup endpoints from being reachable.");
ConfigurationManager.Configuration.IsStartupWizardCompleted = true;
ConfigurationManager.SaveConfiguration();
}
}
/// <inheritdoc/>
public void Init(IServiceCollection serviceCollection)
{
@@ -965,8 +987,9 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
{
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (hostname.StartsWith("http", StringComparison.OrdinalIgnoreCase))
// If the smartAPI isn't already a complete URL then treat it as a host or ip.
if (hostname.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| hostname.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
return hostname.TrimEnd('/');
}
@@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.IO;
@@ -10,6 +8,9 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Devices
{
/// <summary>
/// Provides the persistent unique identifier of this server installation.
/// </summary>
public class DeviceId
{
private readonly IApplicationPaths _appPaths;
@@ -18,12 +19,20 @@ namespace Emby.Server.Implementations.Devices
private string? _id;
/// <summary>
/// Initializes a new instance of the <see cref="DeviceId"/> class.
/// </summary>
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DeviceId}"/> interface.</param>
public DeviceId(IApplicationPaths appPaths, ILogger<DeviceId> logger)
{
_appPaths = appPaths;
_logger = logger;
}
/// <summary>
/// Gets the device id, loading it from disk or generating and persisting a new one if none exists.
/// </summary>
public string Value => _id ??= GetDeviceId();
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
+105 -35
View File
@@ -71,6 +71,8 @@ namespace Emby.Server.Implementations.Dto
{
BaseItemKind.Person, [
BaseItemKind.Audio,
BaseItemKind.AudioBook,
BaseItemKind.Book,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
@@ -167,9 +169,13 @@ namespace Emby.Server.Implementations.Dto
// Batch-fetch user data for all items
Dictionary<Guid, UserItemData>? userDataBatch = null;
IReadOnlyDictionary<Guid, VersionResumeData>? resumeDataBatch = null;
if (user is not null && options.EnableUserData)
{
userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
// For items with alternate versions, the most recently played version drives resume.
resumeDataBatch = _userDataRepository.GetResumeUserDataBatch(accessibleItems, user);
}
// Pre-compute collection folders once to avoid N+1 queries in CanDelete
@@ -236,6 +242,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];
@@ -248,7 +277,10 @@ namespace Emby.Server.Implementations.Dto
allCollectionFolders,
childCountBatch,
playedCountBatch,
artistsBatch);
artistsBatch,
resumeDataBatch?.GetValueOrDefault(item.Id),
peopleBatch,
alternateVersionItemIds);
if (item is LiveTvChannel tvChannel)
{
@@ -309,7 +341,10 @@ namespace Emby.Server.Implementations.Dto
List<Folder>? allCollectionFolders = null,
Dictionary<Guid, int>? childCountBatch = null,
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null,
VersionResumeData? resumeData = null,
IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null,
IReadOnlySet<Guid>? alternateVersionItemIds = null)
{
var dto = new BaseItemDto
{
@@ -323,7 +358,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))
@@ -353,7 +396,8 @@ namespace Emby.Server.Implementations.Dto
options,
userData,
childCountBatch,
playedCountBatch);
playedCountBatch,
resumeData);
}
if (item is IHasMediaSources
@@ -369,7 +413,7 @@ namespace Emby.Server.Implementations.Dto
AttachStudios(dto, item);
}
AttachBasicFields(dto, item, owner, options, artistsBatch);
AttachBasicFields(dto, item, owner, options, artistsBatch, user, alternateVersionItemIds);
if (options.ContainsField(ItemFields.CanDelete))
{
@@ -538,7 +582,8 @@ namespace Emby.Server.Implementations.Dto
DtoOptions options,
UserItemData? userData = null,
Dictionary<Guid, int>? childCountBatch = null,
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
VersionResumeData? resumeData = null)
{
if (item.IsFolder)
{
@@ -600,6 +645,9 @@ namespace Emby.Server.Implementations.Dto
// Use pre-fetched user data
dto.UserData = GetUserItemDataDto(userData, item.Id);
item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
// For items with alternate versions, the most recently played version drives resume.
resumeData?.ApplyTo(dto.UserData);
}
else
{
@@ -729,12 +777,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))
@@ -943,7 +997,9 @@ namespace Emby.Server.Implementations.Dto
/// <param name="owner">The owner.</param>
/// <param name="options">The options.</param>
/// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
/// <param name="user">The user, for per-user values such as the accessible media source count.</param>
/// <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))
{
@@ -1074,7 +1130,7 @@ namespace Emby.Server.Implementations.Dto
dto.ParentId = item.DisplayParentId;
}
AddInheritedImages(dto, item, options, owner);
AddInheritedImages(dto, item, options, owner, artistsBatch);
if (options.ContainsField(ItemFields.Path))
{
@@ -1257,10 +1313,27 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.MediaSourceCount))
{
var mediaSourceCount = video.MediaSourceCount;
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;
}
}
}
@@ -1366,38 +1439,22 @@ namespace Emby.Server.Implementations.Dto
}
}
if (options.PreferEpisodeParentPoster)
if (options.GetImageLimit(ImageType.Primary) > 0)
{
var episodeSeason = episode.Season;
var seasonPrimaryTag = episodeSeason is not null
? GetTagAndFillBlurhash(dto, episodeSeason, ImageType.Primary)
: null;
BaseItem? posterParent = null;
if (seasonPrimaryTag is not null)
{
dto.ParentPrimaryImageItemId = episodeSeason!.Id;
dto.ParentPrimaryImageTag = seasonPrimaryTag;
posterParent = episodeSeason;
}
else if (episodeSeries is not null && dto.SeriesPrimaryImageTag is not null)
{
dto.ParentPrimaryImageItemId = episodeSeries.Id;
dto.ParentPrimaryImageTag = dto.SeriesPrimaryImageTag;
posterParent = episodeSeries;
}
if (posterParent is not null)
{
if (dto.ImageTags is not null && dto.ImageTags.Remove(ImageType.Primary, out var ownPrimaryTag))
{
// Only drop the episode's own primary blurhash; keep the poster parent's.
dto.ImageBlurHashes?.GetValueOrDefault(ImageType.Primary)?.Remove(ownPrimaryTag);
}
dto.SeriesPrimaryImageTag = null;
dto.PrimaryImageAspectRatio = null;
AttachPrimaryImageAspectRatio(dto, posterParent);
}
}
@@ -1516,11 +1573,11 @@ namespace Emby.Server.Implementations.Dto
}
}
private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch)
{
if (currentItem is MusicAlbum musicAlbum)
{
var artist = musicAlbum.GetMusicArtist(new DtoOptions(false));
var artist = GetBatchedAlbumArtist(musicAlbum, artistsBatch) ?? musicAlbum.GetMusicArtist(new DtoOptions(false));
if (artist is not null)
{
return artist;
@@ -1537,7 +1594,20 @@ namespace Emby.Server.Implementations.Dto
return parent;
}
private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner)
private static MusicArtist? GetBatchedAlbumArtist(MusicAlbum album, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch)
{
if (artistsBatch is null)
{
return null;
}
var name = album.AlbumArtists.Count > 0 ? album.AlbumArtists[0] : null;
return !string.IsNullOrEmpty(name) && artistsBatch.TryGetValue(name, out var artists) && artists.Length > 0
? artists[0]
: null;
}
private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch)
{
if (item is UserView { ViewType: CollectionType.playlists } playlistsView
&& options.GetImageLimit(ImageType.Primary) > 0
@@ -1582,7 +1652,7 @@ namespace Emby.Server.Implementations.Dto
|| (!(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
|| parent is Series)
{
parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
parent ??= isFirst ? GetImageDisplayParent(item, item, artistsBatch) ?? owner : parent;
if (parent is null)
{
break;
@@ -1641,7 +1711,7 @@ namespace Emby.Server.Implementations.Dto
break;
}
parent = GetImageDisplayParent(parent, item);
parent = GetImageDisplayParent(parent, item, artistsBatch);
}
}
@@ -127,8 +127,12 @@ namespace Emby.Server.Implementations.HttpServer
{
receiveResult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
}
catch (WebSocketException ex)
catch (Exception ex) when (ex is WebSocketException or ObjectDisposedException or OperationCanceledException)
{
// ObjectDisposedException/OperationCanceledException: the socket was torn
// down underneath us (e.g. by the keep-alive watchdog after the connection
// was declared lost). Fall through so Closed is still raised and the
// session can release this connection.
_logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message);
break;
}
@@ -691,7 +691,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex) when (ex is UnauthorizedAccessException or DirectoryNotFoundException or SecurityException)
{
_logger.LogError(ex, "Failed to enumerate path {Path}", path);
_logger.LogWarning("Failed to enumerate path \"{Path}\": {Message}", path, ex.Message);
return Enumerable.Empty<string>();
}
}
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using Jellyfin.Api.Extensions;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
@@ -14,7 +15,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
@@ -28,51 +28,23 @@ namespace Emby.Server.Implementations.Images
{
var view = (CollectionFolder)item;
var viewType = view.CollectionType;
BaseItemKind[] includeItemTypes;
switch (viewType)
{
case CollectionType.movies:
includeItemTypes = new[] { BaseItemKind.Movie };
break;
case CollectionType.tvshows:
includeItemTypes = new[] { BaseItemKind.Series };
break;
case CollectionType.music:
includeItemTypes = new[] { BaseItemKind.MusicArtist }; // Music albums usually don't have dedicated backdrops, so use artist instead
break;
case CollectionType.musicvideos:
includeItemTypes = new[] { BaseItemKind.MusicVideo };
break;
case CollectionType.books:
includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
break;
case CollectionType.boxsets:
includeItemTypes = new[] { BaseItemKind.BoxSet };
break;
case CollectionType.homevideos:
case CollectionType.photos:
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
break;
default:
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
break;
}
var includeItemTypes = DtoExtensions.GetBaseItemKindsForCollectionType(viewType);
var recursive = viewType != CollectionType.playlists;
if (viewType == CollectionType.music)
{
// Music albums usually don't have dedicated backdrops, so use artist instead
includeItemTypes = [BaseItemKind.MusicArtist];
}
return view.GetItemList(new InternalItemsQuery
{
CollapseBoxSetItems = false,
Recursive = recursive,
DtoOptions = new DtoOptions(false),
ImageTypes = new[] { ImageType.Primary },
ImageTypes = [ImageType.Primary],
Limit = 8,
OrderBy = new[]
{
(ItemSortBy.Random, SortOrder.Ascending)
},
OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)],
IncludeItemTypes = includeItemTypes
});
}
@@ -45,6 +45,7 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
@@ -86,6 +87,7 @@ namespace Emby.Server.Implementations.Library
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager;
private readonly ILocalizationManager _localization;
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
private readonly IMediaStreamRepository _mediaStreamRepository;
@@ -132,6 +134,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="peopleRepository">The people repository.</param>
/// <param name="pathManager">The path manager.</param>
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
/// <param name="localization">The localization manager.</param>
/// <param name="mediaStreamRepository">The media stream repository.</param>
/// <param name="externalDataManagerFactory">The external data manager (lazy, to break the DI cycle through ChapterManager).</param>
public LibraryManager(
@@ -157,6 +160,7 @@ namespace Emby.Server.Implementations.Library
IPeopleRepository peopleRepository,
IPathManager pathManager,
DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
ILocalizationManager localization,
IMediaStreamRepository mediaStreamRepository,
Lazy<IExternalDataManager> externalDataManagerFactory)
{
@@ -184,6 +188,7 @@ namespace Emby.Server.Implementations.Library
_peopleRepository = peopleRepository;
_pathManager = pathManager;
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
_localization = localization;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -407,6 +412,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)
@@ -606,6 +618,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);
}
@@ -2229,6 +2247,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)
{
@@ -2315,9 +2339,13 @@ namespace Emby.Server.Implementations.Library
{
var comparer = Comparers.FirstOrDefault(c => name == c.Type);
// If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
{
if (user is null)
{
throw new ArgumentException($"Sort key '{name}' requires a user, but none was provided.");
}
var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null for Nullable<T> instances
userComparer.User = user;
@@ -2367,6 +2395,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.
@@ -2552,6 +2581,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);
@@ -2580,6 +2611,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.
@@ -2640,6 +2672,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)
{
@@ -3195,11 +3251,11 @@ namespace Emby.Server.Implementations.Library
}
}
if (!episode.ProductionYear.HasValue)
if (episode.ProductionYear is null)
{
episode.ProductionYear = episodeInfo.Year;
if (episode.ProductionYear.HasValue)
if (episode.ProductionYear is not null)
{
changed = true;
}
@@ -3276,9 +3332,11 @@ namespace Emby.Server.Implementations.Library
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null)
{
yield break;
return [];
}
var candidates = new List<ExtraCandidate>();
var count = filtered.Count;
for (var i = 0; i < count; i++)
{
@@ -3292,35 +3350,50 @@ namespace Emby.Server.Implementations.Library
foreach (var file in filesInSubFolderList)
{
if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType, out var extraRule))
{
continue;
}
var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder);
if (extra is not null)
{
yield return extra;
}
AddCandidate(file, extraType.Value, extraRule, subFolderIsMixedFolder);
}
}
else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType))
else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType, out var extraRule))
{
var extra = GetExtra(current, extraType.Value, false);
if (extra is not null)
{
yield return extra;
}
AddCandidate(current, extraType.Value, extraRule, false);
}
}
BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder)
var extras = new List<BaseItem>();
var typeCounters = new Dictionary<ExtraType, int>();
// Order by path so that the numbering handed out below does not depend on the
// order the file system happened to list the folder in
foreach (var candidate in candidates.OrderBy(c => c.Extra.Path, StringComparer.Ordinal))
{
var extra = PrepareExtra(candidate);
if (extra is not null)
{
extras.Add(extra);
}
}
return extras;
void AddCandidate(FileSystemMetadata file, ExtraType extraType, ExtraRule extraRule, bool isInMixedFolder)
{
var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType));
if (extra is not Video && extra is not Audio)
if (extra is Video or Audio)
{
return null;
candidates.Add(new ExtraCandidate(extra, extraType, extraRule, isInMixedFolder));
}
}
BaseItem? PrepareExtra(ExtraCandidate candidate)
{
var resolved = candidate.Extra;
var extra = resolved;
var name = GetExtraName(candidate, ownerVideoInfo, typeCounters);
// Try to retrieve it from the db. If we don't find it, use the resolved version
var itemById = GetItemById(extra.Id);
@@ -3329,10 +3402,18 @@ namespace Emby.Server.Implementations.Library
extra = itemById;
}
// Only update extra type if it is more specific then the currently known extra type
if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
// An extra is named after its file, so the file is the source of truth. Items created
// by older versions, or renamed by a metadata provider, are corrected here;
// RefreshExtras persists the change.
if (!string.IsNullOrEmpty(name) && extra.LockedFields?.Contains(MetadataField.Name) != true)
{
extra.ExtraType = extraType;
extra.Name = name;
}
// Only update extra type if it is more specific then the currently known extra type
if (extra.ExtraType is null or ExtraType.Unknown || candidate.ExtraType != ExtraType.Unknown)
{
extra.ExtraType = candidate.ExtraType;
}
// Only return items that are actual extras (have ExtraType set)
@@ -3340,7 +3421,7 @@ namespace Emby.Server.Implementations.Library
// so that RefreshExtras can detect when they need updating and set ForceSave.
if (extra.ExtraType is not null)
{
extra.IsInMixedFolder = isInMixedFolder;
extra.IsInMixedFolder = candidate.IsInMixedFolder;
return extra;
}
@@ -3348,6 +3429,57 @@ namespace Emby.Server.Implementations.Library
}
}
/// <summary>
/// Gets the name to give an extra.
/// </summary>
/// <param name="candidate">The resolved extra.</param>
/// <param name="ownerVideoInfo">The naming info of the owner.</param>
/// <param name="typeCounters">Number of extras named after their type so far, per type.</param>
/// <returns>The name.</returns>
private string GetExtraName(ExtraCandidate candidate, VideoFileInfo ownerVideoInfo, Dictionary<ExtraType, int> typeCounters)
{
var isNamedAfterOwner = candidate.ExtraRule.RuleType switch
{
ExtraRuleType.Filename => true,
ExtraRuleType.Suffix => string.Equals(candidate.Extra.Name, ownerVideoInfo.Name, StringComparison.OrdinalIgnoreCase),
_ => false
};
if (!isNamedAfterOwner)
{
return candidate.Extra.Name;
}
typeCounters.TryGetValue(candidate.ExtraType, out var seen);
typeCounters[candidate.ExtraType] = seen + 1;
var typeName = _localization.GetServerLocalizedString(GetExtraTypeNameKey(candidate.ExtraType));
return seen == 0
? typeName
: string.Format(
CultureInfo.InvariantCulture,
_localization.GetServerLocalizedString("NameExtraNumbered"),
typeName,
seen + 1);
}
private static string GetExtraTypeNameKey(ExtraType extraType) => extraType switch
{
ExtraType.Clip => "NameExtraClip",
ExtraType.Trailer => "NameExtraTrailer",
ExtraType.BehindTheScenes => "NameExtraBehindTheScenes",
ExtraType.DeletedScene => "NameExtraDeletedScene",
ExtraType.Interview => "NameExtraInterview",
ExtraType.Scene => "NameExtraScene",
ExtraType.Sample => "NameExtraSample",
ExtraType.ThemeSong => "NameExtraThemeSong",
ExtraType.ThemeVideo => "NameExtraThemeVideo",
ExtraType.Featurette => "NameExtraFeaturette",
ExtraType.Short => "NameExtraShort",
_ => "NameExtraUnknown"
};
public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
{
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
@@ -3424,6 +3556,12 @@ namespace Emby.Server.Implementations.Library
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();
@@ -3886,5 +4024,19 @@ namespace Emby.Server.Implementations.Library
{
return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
}
/// <inheritdoc />
public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType, InternalItemsQuery query)
{
if (query.User is not null)
{
AddUserToQuery(query, query.User);
}
SetTopParentOrAncestorIds(query);
return _itemRepository.GetMediaStreamLanguages(query, mediaStreamType);
}
private sealed record ExtraCandidate(BaseItem Extra, ExtraType ExtraType, ExtraRule ExtraRule, bool IsInMixedFolder);
}
}
@@ -229,7 +229,11 @@ namespace Emby.Server.Implementations.Library
list.Add(source);
}
return SortMediaSources(list, item.Id).ToArray();
var preferredId = mediaSources.Count > 0 && Guid.TryParse(mediaSources[0].Id, out var topSourceId)
? topSourceId
: item.Id;
return SortMediaSources(list, preferredId).ToArray();
}
/// <inheritdoc />>
@@ -406,6 +410,59 @@ namespace Emby.Server.Implementations.Library
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
}
}
sources = SetAlternateVersionResumeStates(item, sources, user);
}
return sources;
}
/// <summary>
/// When the queried item is a primary, moves the most recently played version to the front so
/// that resuming without an explicit source selection plays the version that was last watched.
/// A directly queried alternate version keeps its own source first. Per-user playback position
/// is not surfaced on the source itself; it is carried by each version's own UserData.
/// </summary>
/// <param name="item">The queried item.</param>
/// <param name="sources">The item's media sources.</param>
/// <param name="user">The user.</param>
/// <returns>The media sources, reordered when a version drives resume.</returns>
private IReadOnlyList<MediaSourceInfo> SetAlternateVersionResumeStates(BaseItem item, IReadOnlyList<MediaSourceInfo> sources, User user)
{
// For a video, multiple sources means alternate versions.
if (item is not Video video || sources.Count < 2)
{
return sources;
}
var versions = video.GetAllVersions();
if (versions.Count < 2)
{
return sources;
}
var userDataByVersion = _userDataManager.GetUserDataBatch(versions, user);
var dataBySourceId = new Dictionary<string, UserItemData>(versions.Count, StringComparer.OrdinalIgnoreCase);
foreach (var version in versions)
{
if (userDataByVersion.TryGetValue(version.Id, out var data))
{
dataBySourceId[version.Id.ToString("N", CultureInfo.InvariantCulture)] = data;
}
}
// Reorder only for a resumable (in-progress) version;
// a completed version has no position to resume, so it must not be pulled to the front here.
var resumeSource = VersionPlaybackSelector.SelectMostRecentlyPlayed(
sources,
source => source.Id is not null ? dataBySourceId.GetValueOrDefault(source.Id) : null,
data => data.PlaybackPositionTicks > 0);
if (resumeSource is not null && !video.PrimaryVersionId.HasValue && !ReferenceEquals(sources[0], resumeSource))
{
var reordered = new List<MediaSourceInfo>(sources.Count) { resumeSource };
reordered.AddRange(sources.Where(s => !ReferenceEquals(s, resumeSource)));
return reordered;
}
return sources;
@@ -29,17 +29,41 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("String can't be empty.", nameof(attribute));
}
var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
// Must be at least 3 characters after the attribute =, ], any character,
// then we offset it by 1, because we want the index and not length.
var maxIndex = str.Length - attribute.Length - 2;
while (attributeIndex > -1 && attributeIndex < maxIndex)
// Allow tmdb as an alias for tmdbid, tvdb for tvdbid, etc.
// The code below only supports aliases for attributes in the form of "<alias>id".
ReadOnlySpan<char> shortAttr = attribute switch
{
var attributeEnd = attributeIndex + attribute.Length;
_ when attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase) => "tmdb",
_ when attribute.Equals("tvdbid", StringComparison.OrdinalIgnoreCase) => "tvdb",
_ when attribute.Equals("imdbid", StringComparison.OrdinalIgnoreCase) => "imdb",
_ => ReadOnlySpan<char>.Empty
};
for (int strIndex = 0, attributeIndex = 0; attributeIndex > -1;)
{
// We may want to use imdbid pattern matching later, so we don't want to modify the original 'str'.
var subStr = str[strIndex..];
int attributeEnd = 0;
if (shortAttr.Length > 0)
{
// If we are using an alias it should be shorter (and a prefix), so let's search for that.
attributeIndex = subStr.IndexOf(shortAttr, StringComparison.OrdinalIgnoreCase);
attributeEnd = attributeIndex + shortAttr.Length;
}
else
{
attributeIndex = subStr.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
attributeEnd = attributeIndex + attribute.Length;
}
// The next iteration should start at the end of the attribute we just found.
// If attributeIndex < 0, the loop will end and strIndex won't be used again.
strIndex += attributeEnd;
if (attributeIndex > 0)
{
var attributeOpener = str[attributeIndex - 1];
var attributeOpener = subStr[attributeIndex - 1];
var attributeCloser = attributeOpener switch
{
'[' => ']',
@@ -47,20 +71,37 @@ namespace Emby.Server.Implementations.Library
'{' => '}',
_ => '\0'
};
if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
{
var closingIndex = str[attributeEnd..].IndexOf(attributeCloser);
// Must be at least 1 character before the closing bracket.
if (closingIndex > 1)
if (attributeCloser != '\0')
{
if (shortAttr.Length > 0
&& attributeEnd + 1 < subStr.Length
&& (subStr[attributeEnd] is 'i' or 'I')
&& (subStr[attributeEnd + 1] is 'd' or 'D'))
{
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
// We were searching for a shortened attribute, but it's followed by "id" - let's skip it.
attributeEnd += 2;
}
// attributeEnd points at '='.
// We need at least 1 more character and the closing bracket after that.
if (attributeEnd + 2 < subStr.Length && (subStr[attributeEnd] is '=' or '-'))
{
var closingIndex = subStr[attributeEnd..].IndexOf(attributeCloser);
// Must be at least 1 character before the closing bracket.
if (closingIndex > 1)
{
var trimmed = subStr[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim();
if (trimmed.Length > 0)
{
return trimmed.ToString();
}
}
}
}
}
str = str[attributeEnd..];
attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
}
// for imdbid we also accept pattern matching
@@ -70,16 +111,6 @@ namespace Emby.Server.Implementations.Library
return match ? imdbId.ToString() : null;
}
// Allow tmdb as an alias for tmdbid
if (attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase))
{
var tmdbValue = str.GetAttributeValue("tmdb");
if (tmdbValue is not null)
{
return tmdbValue;
}
}
return null;
}
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -43,7 +44,19 @@ public class PathManager : IPathManager
public string? GetAttachmentPath(string mediaSourceId, string fileName)
{
var folder = GetAttachmentFolderPath(mediaSourceId);
return folder is null ? null : Path.Combine(folder, fileName);
if (folder is null)
{
return null;
}
var safeName = PathHelper.GetSafeLeafFileName(fileName);
if (safeName is null)
{
_logger.LogWarning("Rejecting attachment filename '{FileName}' for MediaSource {MediaSourceId}: not a valid leaf name.", fileName, mediaSourceId);
return null;
}
return Path.Combine(folder, safeName);
}
/// <inheritdoc />
@@ -32,8 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
: base(logger, namingOptions, directoryService)
{
_namingOptions = namingOptions;
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
_videoResolvers = new IItemResolver[] { this };
_trailerResolvers = [new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService, parseName: true)];
_videoResolvers = [this];
}
protected override Video Resolve(ItemResolveArgs args)
@@ -54,12 +54,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
_ => _videoResolvers
};
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, [NotNullWhen(true)] out ExtraRule? extraRule, string? libraryRoot = "")
{
var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
if (extraResult.ExtraType is null)
if (extraResult.ExtraType is null || extraResult.Rule is null)
{
extraType = null;
extraRule = null;
return false;
}
@@ -88,6 +89,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
extraType = extraResult.ExtraType;
extraRule = extraResult.Rule;
return isValid;
}
@@ -2,6 +2,7 @@
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -14,15 +15,25 @@ namespace Emby.Server.Implementations.Library.Resolvers
public class GenericVideoResolver<T> : BaseVideoResolver<T>
where T : Video, new()
{
private readonly bool _parseName;
/// <summary>
/// Initializes a new instance of the <see cref="GenericVideoResolver{T}"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
/// <param name="parseName">Whether to parse the file name for metadata such as the year.</param>
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService, bool parseName = false)
: base(logger, namingOptions, directoryService)
{
_parseName = parseName;
}
/// <inheritdoc />
protected override T Resolve(ItemResolveArgs args)
{
return ResolveVideo<T>(args, _parseName);
}
}
}
@@ -376,15 +376,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// We need to only look at the name of this actual item (not parents)
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
var tmdbid = justName.GetAttributeValue("tmdbid");
// The fallback filename is only used when the item isn't in a mixed folder
var fileName = item.IsInMixedFolder ? ReadOnlySpan<char>.Empty : Path.GetFileName(item.Path.AsSpan());
// If not in a mixed folder and ID not found in folder path, check filename
if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
item.TrySetProviderId(MetadataProvider.Tmdb, GetIdFromNameOrPath(justName, fileName, "tmdbid"));
item.TrySetProviderId(MetadataProvider.Tvdb, GetIdFromNameOrPath(justName, fileName, "tvdbid"));
string GetIdFromNameOrPath(ReadOnlySpan<char> name, ReadOnlySpan<char> fallbackName, string attribute)
{
tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid");
}
var id = name.GetAttributeValue(attribute);
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
// If not in a mixed folder and ID not found in folder path, check filename
if (string.IsNullOrEmpty(id) && !item.IsInMixedFolder)
{
id = fallbackName.GetAttributeValue(attribute);
}
return id;
}
if (!string.IsNullOrEmpty(item.Path))
{
@@ -1,8 +1,10 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
@@ -45,8 +47,30 @@ namespace Emby.Server.Implementations.Library.Resolvers
};
}
// Anything directly inside the internal playlists folder is a playlist, even when its
// playlist.xml is missing: failing to resolve here makes the library scan treat the
// playlist as deleted from disk and remove it, taking its items with it.
if (args.Parent is PlaylistsFolder)
{
return new Playlist
{
Path = args.Path,
Name = filename,
OpenAccess = true
};
}
// It's a directory-based playlist if the directory contains a playlist file
var filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
IEnumerable<string> filePaths;
try
{
filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
}
catch (IOException)
{
return null;
}
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
{
return new Playlist
@@ -92,49 +92,46 @@ 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;
}
var internalResults = await internalTask.ConfigureAwait(false);
if (_internalProviders.Length > 0)
{
_logger.LogDebug("No results from external providers, using internal provider results");
}
return internalResults;
}
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)];
@@ -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);
}
@@ -53,6 +53,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IItemQueryHelpers _queryHelpers;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="MovieSimilarItemsProvider"/> class.
@@ -60,14 +61,17 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
/// <param name="dbProvider">The database context factory.</param>
/// <param name="queryHelpers">The shared query helpers.</param>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
/// <param name="libraryManager">The library manager.</param>
public MovieSimilarItemsProvider(
IDbContextFactory<JellyfinDbContext> dbProvider,
IItemQueryHelpers queryHelpers,
IServerConfigurationManager serverConfigurationManager)
IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager)
{
_dbProvider = dbProvider;
_queryHelpers = queryHelpers;
_serverConfigurationManager = serverConfigurationManager;
_libraryManager = libraryManager;
}
/// <inheritdoc/>
@@ -156,6 +160,11 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
IsPlayed = false
};
if (query.User is not null)
{
_libraryManager.ConfigureUserAccess(filter, query.User);
}
_queryHelpers.PrepareFilterQuery(filter);
var baseQuery = _queryHelpers.PrepareItemQuery(context, filter);
baseQuery = _queryHelpers.TranslateQuery(baseQuery, context, filter);
@@ -183,6 +183,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 +200,7 @@ public class SimilarItemsManager : ISimilarItemsManager
remaining -= resolvedItems.Count;
pendingBatch.Clear();
if (remaining <= 0)
if (remaining <= 0 || collectedReferences.Count >= MaxRemoteReferenceFetchLimit)
{
break;
}
@@ -192,7 +192,8 @@ namespace Emby.Server.Implementations.Library
}
else
{
var userData = item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault();
var userDataRow = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
var userData = userDataRow is not null ? Map(userDataRow) : null;
if (userData is not null)
{
result[item.Id] = userData;
@@ -211,36 +212,128 @@ namespace Emby.Server.Implementations.Library
return result;
}
// Build a single query for all missing items
// Build a single query for all missing items. Fetch rows by item alone so rows kept
// under keys from older metadata resolve the same way as the in-memory path.
var allItemIds = itemsNeedingQuery.Select(x => x.Item.Id).ToList();
var allKeys = itemsNeedingQuery.SelectMany(x => x.Keys).Distinct().ToList();
if (allKeys.Count > 0)
using var context = _repository.CreateDbContext();
var userDataArray = context.UserData
.AsNoTracking()
.Where(e => e.UserId.Equals(user.Id))
.WhereOneOrMany(allItemIds, e => e.ItemId)
.ToArray();
var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray());
foreach (var (item, keys) in itemsNeedingQuery)
{
using var context = _repository.CreateDbContext();
var userDataArray = context.UserData
.AsNoTracking()
.Where(e => e.UserId.Equals(user.Id))
.WhereOneOrMany(allItemIds, e => e.ItemId)
.WhereOneOrMany(allKeys, e => e.CustomDataKey)
.ToArray();
var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray());
foreach (var (item, keys) in itemsNeedingQuery)
UserItemData userData;
if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0)
{
UserItemData userData;
if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0)
{
var directDataReference = itemUserData.FirstOrDefault(e => e.CustomDataKey == item.Id.ToString("N"));
userData = directDataReference is not null ? Map(directDataReference) : Map(itemUserData.First());
}
else
{
userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty };
}
userData = Map(ResolveUserDataRow(item, itemUserData)!);
}
else
{
userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty };
}
result[item.Id] = userData;
var cacheKey = GetCacheKey(user.InternalId, item.Id);
_cache.AddOrUpdate(cacheKey, userData);
result[item.Id] = userData;
var cacheKey = GetCacheKey(user.InternalId, item.Id);
_cache.AddOrUpdate(cacheKey, userData);
}
return result;
}
/// <inheritdoc />
public VersionResumeData? GetResumeUserData(User user, BaseItem item)
{
return GetResumeUserDataBatch([item], user).GetValueOrDefault(item.Id);
}
/// <inheritdoc />
public IReadOnlyDictionary<Guid, VersionResumeData> GetResumeUserDataBatch(IReadOnlyList<BaseItem> items, User user)
{
ArgumentNullException.ThrowIfNull(user);
var result = new Dictionary<Guid, VersionResumeData>();
// Candidate primaries: a directly queried version (PrimaryVersionId set) keeps its own data.
// Linked alternates are already known in memory; only the local-alternate existence check
// would otherwise hit the database (one query per item via Video.HasLocalAlternateVersions),
// so collect those ids and resolve them all in a single query below.
List<Video>? candidates = null;
List<Guid>? localProbeIds = null;
foreach (var item in items)
{
if (item is not Video video || video.PrimaryVersionId.HasValue)
{
continue;
}
(candidates ??= []).Add(video);
if (video.LinkedAlternateVersions.Length == 0)
{
(localProbeIds ??= []).Add(video.Id);
}
}
if (candidates is null)
{
return result;
}
HashSet<Guid>? withLocalAlternates = null;
if (localProbeIds is not null)
{
using var dbContext = _repository.CreateDbContext();
withLocalAlternates = dbContext.LinkedChildren
.Where(lc => lc.ChildType == Jellyfin.Database.Implementations.Entities.LinkedChildType.LocalAlternateVersion)
.WhereOneOrMany(localProbeIds, lc => lc.ParentId)
.Select(lc => lc.ParentId)
.Distinct()
.ToHashSet();
}
List<(Guid PrimaryId, IReadOnlyList<Video> Versions)>? versionGroups = null;
List<BaseItem>? allVersions = null;
foreach (var video in candidates)
{
// Only items that actually have alternate versions aggregate over them.
if (video.LinkedAlternateVersions.Length == 0
&& (withLocalAlternates is null || !withLocalAlternates.Contains(video.Id)))
{
continue;
}
var versions = video.GetAllVersions();
if (versions.Count < 2)
{
continue;
}
(versionGroups ??= []).Add((video.Id, versions));
(allVersions ??= []).AddRange(versions);
}
if (versionGroups is null)
{
return result;
}
var userDataByVersion = GetUserDataBatch(allVersions!.DistinctBy(i => i.Id).ToList(), user);
foreach (var (primaryId, versions) in versionGroups)
{
// Consider both in-progress and completed versions so a finished alternate still marks the primary as played.
var resumeVersion = VersionPlaybackSelector.SelectMostRecentlyPlayed(
versions,
version => userDataByVersion.GetValueOrDefault(version.Id),
data => data.PlaybackPositionTicks > 0 || data.Played);
if (resumeVersion is not null)
{
result[primaryId] = new VersionResumeData(resumeVersion.Id, userDataByVersion[resumeVersion.Id]);
}
}
@@ -259,12 +352,41 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public UserItemData? GetUserData(User user, BaseItem item)
{
return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData()
ArgumentNullException.ThrowIfNull(user);
var row = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
return row is not null ? Map(row) : new UserItemData()
{
Key = item.GetUserDataKeys()[0],
};
}
/// <summary>
/// Picks the row matching the item's current user data keys, in key order, so rows left behind
/// under keys from older metadata don't take priority over the rows the write path updates.
/// </summary>
/// <param name="item">The item whose keys to match.</param>
/// <param name="rows">The candidate user data rows for a single user.</param>
/// <returns>The best matching row, or <c>null</c> when there are none.</returns>
private static UserData? ResolveUserDataRow(BaseItem item, IEnumerable<UserData>? rows)
{
var candidates = rows?.ToList();
if (candidates is null || candidates.Count == 0)
{
return null;
}
foreach (var key in item.GetUserDataKeys())
{
var match = candidates.Find(e => string.Equals(e.CustomDataKey, key, StringComparison.Ordinal));
if (match is not null)
{
return match;
}
}
return candidates[0];
}
/// <inheritdoc />
public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
=> GetUserDataDto(item, null, user, new DtoOptions());
@@ -281,6 +403,10 @@ namespace Emby.Server.Implementations.Library
var dto = GetUserItemDataDto(userData, item.Id);
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
// For an item with alternate versions, surface the most recently played version's resume point.
GetResumeUserData(user, item)?.ApplyTo(dto);
return dto;
}
@@ -385,5 +511,41 @@ namespace Emby.Server.Implementations.Library
return playedToCompletion;
}
/// <inheritdoc />
public void ResetPlaybackStreamSelections(User user, BaseItem item)
{
ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(item);
using var dbContext = _repository.CreateDbContext();
var rows = dbContext.UserData
.Where(e => e.ItemId == item.Id && e.UserId == user.Id
&& (e.AudioStreamIndex != null || e.SubtitleStreamIndex != null))
.ToList();
if (rows.Count == 0)
{
return;
}
foreach (var row in rows)
{
row.AudioStreamIndex = null;
row.SubtitleStreamIndex = null;
}
dbContext.SaveChanges();
var cacheKey = GetCacheKey(user.InternalId, item.Id);
if (_cache.TryGet(cacheKey, out var cached))
{
cached.AudioStreamIndex = null;
cached.SubtitleStreamIndex = null;
_cache.AddOrUpdate(cacheKey, cached);
}
item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray();
}
}
}
@@ -0,0 +1,64 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Ensures top-level library folders have a primary poster after scans.
/// Poster extraction is attempted before library scanning. When a library is
/// empty at that point, no poster can be extracted. This post-scan task reruns
/// metadata extraction for top-level folders that are still missing images.
/// </summary>
public class CollectionPosterVerifyPostScanTask : ILibraryPostScanTask
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger<CollectionPosterVerifyPostScanTask> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="CollectionPosterVerifyPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
public CollectionPosterVerifyPostScanTask(
ILibraryManager libraryManager,
ILogger<CollectionPosterVerifyPostScanTask> logger)
{
_libraryManager = libraryManager;
_logger = logger;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var libraries = _libraryManager.GetUserRootFolder().Children.OfType<CollectionFolder>().ToList();
var totalLibraries = libraries.Count;
var processedLibraries = 0;
foreach (var library in libraries)
{
cancellationToken.ThrowIfCancellationRequested();
if (!library.HasImage(ImageType.Primary))
{
_logger.LogDebug("Library {LibraryName} is missing a primary image. Refreshing metadata.", library.Name);
await library.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
processedLibraries++;
progress.Report((double)processedLibraries / totalLibraries * 100);
}
progress.Report(100);
}
}
@@ -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": "إضافي"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari",
"Original": "Original",
"LyricDownloadFailureFromForItem": "No s'han pogut descarregar les lletres des de {0} per a {1}"
"LyricDownloadFailureFromForItem": "No s'han pogut descarregar les lletres des de {0} per a {1}",
"NameExtraBehindTheScenes": "Rere les càmeres",
"NameExtraClip": "Tall",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Migmetratge",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Mostra",
"NameExtraScene": "Escena",
"NameExtraShort": "Curt",
"NameExtraThemeSong": "Tema musical",
"NameExtraThemeVideo": "Vídeo temàtic",
"NameExtraTrailer": "Tràiler",
"NameExtraUnknown": "Extra"
}
@@ -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"
}
@@ -9,7 +9,7 @@
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
"HeaderContinueWatching": "Fortsæt afspilning",
"HeaderContinueWatching": "Fortsæt med at se",
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
"HeaderFavoriteShows": "Yndlingsserier",
"HeaderLiveTV": "Live-TV",
@@ -65,7 +65,7 @@
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er konfigurerede til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater plugins",
"TaskUpdatePlugins": "Opdatér plugins",
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
"TaskCleanLogs": "Ryd log-mappe",
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
@@ -79,10 +79,10 @@
"TaskRefreshChapterImages": "Udtræk kapitelbilleder",
"TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
"TaskRefreshChannelsDescription": "Opdaterer information for internetkanaler.",
"TaskRefreshChannels": "Opdater kanaler",
"TaskRefreshChannels": "Opdatér kanaler",
"TaskCleanTranscodeDescription": "Fjerner omkodningsfiler, som er mere end 1 dag gamle.",
"TaskCleanTranscode": "Tøm omkodningsmappen",
"TaskRefreshPeople": "Opdater personer",
"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.",
"TaskCleanActivityLog": "Ryd aktivitetslog",
@@ -90,7 +90,7 @@
"Forced": "Tvunget",
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimerer databasen for at frigøre plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen.",
"TaskOptimizeDatabase": "Optimer database",
"TaskOptimizeDatabase": "Optimér database",
"TaskKeyframeExtractorDescription": "Udtrækker rammer fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
"TaskKeyframeExtractor": "Udtræk nøglerammer",
"External": "Ekstern",
@@ -99,7 +99,7 @@
"TaskRefreshTrickplayImagesDescription": "Laver trickplay-billeder for videoer i aktiverede biblioteker.",
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende lydnormalisering.",
"TaskAudioNormalization": "Lydnormalisering",
"TaskDownloadMissingLyricsDescription": "Søger på internettet efter manglende sangtekster baseret på metadata-konfigurationen",
"TaskDownloadMissingLyricsDescription": "Download sangtekster",
"TaskDownloadMissingLyrics": "Hent manglende sangtekster",
"TaskExtractMediaSegments": "Scan for mediesegmenter",
"TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder",
@@ -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": "Tema Sang",
"NameExtraThemeVideo": "Tema Video",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Ekstra"
}
@@ -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"
}
@@ -28,6 +28,19 @@
"Movies": "Movies",
"Music": "Music",
"MusicVideos": "Music Videos",
"NameExtraBehindTheScenes": "Behind The Scenes",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Deleted Scene",
"NameExtraFeaturette": "Featurette",
"NameExtraInterview": "Interview",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Sample",
"NameExtraScene": "Scene",
"NameExtraShort": "Short",
"NameExtraThemeSong": "Theme Song",
"NameExtraThemeVideo": "Theme Video",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",
@@ -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}"
}
@@ -106,5 +106,20 @@
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"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."
"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",
"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,11 @@
"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"
}
@@ -1,7 +1,7 @@
{
"Artists": "Listafólk",
"Artists": "Tónlistafólk",
"Collections": "Søvn",
"Default": "Sjálvgildi",
"Default": "Forsett",
"External": "Ytri",
"Genres": "Greinar",
"AppDeviceValues": "App: {0}, Eind: {1}",
@@ -12,5 +12,104 @@
"Forced": "Kravt",
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
"HeaderFavoriteEpisodes": "Yndispartar",
"LabelIpAddressValue": "IP atsetur: {0}"
"LabelIpAddressValue": "IP-atsetur: {0}",
"AuthenticationSucceededWithUserName": "{0} varð samgildur",
"HeaderFavoriteShows": "Yndisrøðir",
"HeaderLiveTV": "Beinleiðis sjónvarp",
"HearingImpaired": "Hoyrnarveik",
"Inherit": "Arvar",
"LabelRunningTimeValue": "Spælitíð: {0}",
"Latest": "Seinastu",
"LyricDownloadFailureFromForItem": "Miseydnaðist at niðurtakað sangtekst fyri {1} frá {0}",
"NameInstallFailed": "{0} innlegging miseydnaðist",
"NewVersionIsAvailable": "Ein nýggj útgáva av Jellyfin ambætaranum er tøk.",
"NotificationOptionNewLibraryContent": "Nýtt tilfar innlagt",
"NotificationOptionPluginInstalled": "Ískoytisforrit innlagt",
"NotificationOptionPluginUninstalled": "Ískoytisforrit strikað",
"NotificationOptionPluginUpdateInstalled": "Ískoytisforrit dagført",
"NotificationOptionUserLockedOut": "Brúkari útihýstur",
"Photos": "Ljósmyndir",
"PluginInstalledWithName": "{0} innlagt",
"PluginUninstalledWithName": "{0} strikað",
"PluginUpdatedWithName": "{0} dagført",
"Shows": "Røðir",
"SubtitleDownloadFailureFromForItem": "Miseydnaðist at niðurtakað undirtekstir til {1} frá {0}",
"TvShows": "Sjónvarpsrøðir",
"UserCreatedWithName": "Brúkari {0} er stovnaður",
"UserDeletedWithName": "Brúkari {0} er strikaður",
"UserDownloadingItemWithValues": "{0} niðurtekur {1}",
"UserLockedOutWithName": "Brúkari {0} er útihýstur",
"VersionNumber": "Útgáva {0}",
"TasksLibraryCategory": "Savn",
"TaskRefreshLibrary": "Skanna miðlasavn",
"TaskCleanLogsDescription": "Strikar gerðalistafílur eldri enn {0} dagar.",
"TaskUpdatePlugins": "Dagfør ískoytisforrit",
"TaskRefreshChannels": "Endurinnles rásir",
"TaskDownloadMissingLyricsDescription": "Niðurtekur sangtekstir",
"Movies": "Filmar",
"MixedContent": "Blandað innihald",
"Music": "Tónleikur",
"UserStartedPlayingItemWithValues": "{0} spælur {1} á {2}",
"HeaderContinueWatching": "Hald áfram at hyggja",
"MusicVideos": "Sjónbandaløg",
"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",
"ScheduledTaskFailedWithName": "{0} miseydnaðist",
"Undefined": "Óskilmarkað",
"TasksMaintenanceCategory": "Viðlíkahald",
"TaskCleanLogs": "Reinsa gerðalistaskjáttu",
"UserOnlineFromDevice": "{0} er íbundin frá {1}",
"HeaderNextUp": "Næst á skránni",
"NotificationOptionPluginError": "Brek í ískoytisforriti",
"NotificationOptionInstallationFailed": "Innleggingarbrek",
"NotificationOptionServerRestartRequired": "Tørvur er á ambætaraendurbyrjan",
"TasksApplicationCategory": "Nýtsluskipan",
"NotificationOptionApplicationUpdateAvailable": "Skipanardagføring er tøk",
"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.",
"UserOfflineFromDevice": "{0} breyt av á {1}",
"UserPasswordChangedWithName": "Loyniorðið hjá brúkaranum {0} er broytt",
"TasksChannelsCategory": "Alnetsrásir",
"TaskCleanActivityLog": "Reinsa virksemisskrá",
"TaskCleanActivityLogDescription": "Strikar skrásetingar eldri enn ásetta aldur.",
"TaskCleanCache": "Reinsa kovaskjáttu",
"TaskCleanCacheDescription": "Strikar kovafílar ið kervið ikki hevur tørv á longur.",
"TaskCleanTranscode": "Reinsa umkotuskjáttu",
"TaskDownloadMissingLyrics": "Niðurtak vantandi sangtekstir",
"TaskDownloadMissingSubtitles": "Niðurtak vantandi undirtekstir",
"CleanupUserDataTaskDescription": "Strikar allar brúkaradátur, so sum spælistøðu, yndislistastøðu o.s.fr., fyri miðlar ið ikki hava verið tøkir í í minsta lagi 90 dagar.",
"CleanupUserDataTask": "Koyrsla ið reinsar brúkaradátur",
"TaskRefreshPeople": "Dagfør persónsupplýsingar",
"TaskRefreshPeopleDescription": "Dagførur metadátur um leikarar og leikstjórar í tínum margmiðlasavni.",
"TaskRefreshChannelsDescription": "Dagførur upplýsingar um alnetsrásir.",
"TaskDownloadMissingSubtitlesDescription": "Leitar á alnótini eftir vantandi undirtekstum grundað á metadátauppsetan.",
"NotificationOptionTaskFailed": "Brek undir fyriskipaðari koyrslu",
"TaskRefreshLibraryDescription": "Skannar títt miðlasavn fyri nýggjum fílum og dagførur metadátur.",
"TaskKeyframeExtractor": "Lyklamyndaúttøka",
"TaskKeyframeExtractorDescription": "Úttekur lyklamyndir frá kykmynda-fílum til tess at byggja nágreiniligari HLS-spælilistar. Koyrslan kann taka langa tíð.",
"TaskOptimizeDatabaseDescription": "Trýstur dátugrunninin saman og loysur tóma goymslu. Koyrslan kann bøta um avrikið, eftir skanning ella aðrar broytingar í savninum ið elva til dátugrunnsbroytingar.",
"TaskRefreshChapterImagesDescription": "Ger smámyndir fyri kykmyndir ið hava kapitlar.",
"TaskRefreshChapterImages": "Kapitlamyndaúttøkur",
"NotificationOptionVideoPlayback": "Kykmyndaspæl byrjað",
"NotificationOptionVideoPlaybackStopped": "Kykmyndaspæl steðgað",
"NotificationOptionAudioPlayback": "Ljóðspæl byrjað",
"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 uppsend",
"NameExtraShort": "Stuttfilmur",
"NameExtraThemeSong": "Eyðkennislag",
"NameExtraTrailer": "Forfilmur",
"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."
}
@@ -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"
}
@@ -106,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.",
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
"CleanupUserDataTask": "משימת ניקוי מידע משתמש"
"CleanupUserDataTask": "משימת ניקוי מידע משתמש",
"LyricDownloadFailureFromForItem": "הורדת המילים מ-{0} עבור {1} נכשלה",
"Original": "מקור"
}
@@ -105,5 +105,6 @@
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य"
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य",
"Original": "असली"
}
@@ -107,5 +107,6 @@
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.",
"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"
"Original": "Original",
"LyricDownloadFailureFromForItem": "Preuzimanje tekstova pjesmi od {0} za {1} nije uspjelo"
}
@@ -108,5 +108,17 @@
"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"
}
@@ -62,7 +62,7 @@
"UserDownloadingItemWithValues": "{0} hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
"Shows": "Þættir",
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
"TaskRefreshChannelsDescription": "Endurhleður upplýsingum netrása.",
"TaskRefreshChannels": "Endurhlaða Rásir",
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
"TaskCleanTranscode": "Hreinsa Umkóðunarmöppu",
@@ -80,7 +80,7 @@
"TasksMaintenanceCategory": "Viðhald",
"Default": "Sjálfgefið",
"TaskCleanActivityLog": "Hreinsa athafnaskrá",
"TaskRefreshPeople": "Endurnýja fólk",
"TaskRefreshPeople": "Endurnýja upplýsingar um fólk",
"TaskDownloadMissingSubtitles": "Sækja texta sem vantar",
"TaskOptimizeDatabase": "Fínstilla gagnagrunn",
"Undefined": "Óskilgreint",
@@ -95,13 +95,31 @@
"TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
"Forced": "Þvingað",
"External": "Útvær",
"TaskRefreshTrickplayImagesDescription": "Býr til hraðspilunarmyndir fyrir myndbönd í virkum söfnum.",
"TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir",
"TaskRefreshTrickplayImagesDescription": "Býr til hraðspilunarmyndir (Trickplay) fyrir myndbönd í virkum söfnum.",
"TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir (Trickplay)",
"TaskAudioNormalization": "Hljóðstöðlun",
"TaskAudioNormalizationDescription": "Leitar að hljóðstöðlunargögnum í skrám.",
"TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
"TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar",
"TaskExtractMediaSegments": "Skönnun efnishluta",
"CleanupUserDataTask": "Hreinsun notendagagna",
"CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga."
"CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga.",
"LyricDownloadFailureFromForItem": "Ekki tókst að niðurhala texta frá {0} fyrir {1}",
"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.",
"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,15 @@
"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"
}
@@ -106,5 +106,7 @@
"TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
"TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。",
"CleanupUserDataTask": "ユーザーデータのクリーンアップタスク",
"CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。"
"CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。",
"LyricDownloadFailureFromForItem": "歌詞",
"Original": "オリジナル"
}
@@ -108,5 +108,8 @@
"CleanupUserDataTask": "사용자 데이터 정리 작업",
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다.",
"LyricDownloadFailureFromForItem": "{1}에 대한 가사를 {0}에서 다운로드하지 못했습니다",
"Original": "원본"
"Original": "원본",
"NameExtraClip": "클립",
"NameExtraDeletedScene": "삭제된 장면",
"NameExtraInterview": "인터뷰"
}
@@ -104,5 +104,9 @@
"TaskKeyframeExtractorDescription": "Extrahéiert Schlësselbiller aus Videodateien, fir méi präzis HLS-Playlisten ze erstellen. Dës Aufgab kann eng längere Zäit daueren.",
"TaskRefreshChannelsDescription": "Aktualiséiert Informatiounen iwwer Internetkanäl.",
"TaskExtractMediaSegmentsDescription": "Extrahéiert oder kritt Mediesegmenter aus Plugins, déi MediaSegment ënnerstëtzen.",
"TaskOptimizeDatabaseDescription": "Kompriméiert dDatebank a schneit de fräie Speicherplatz zou. Dës Aufgab no engem Bibliothéik-Scan oder anere Ännerungen, déi Datebankmodifikatioune mat sech bréngen, auszeféieren, kann dPerformance verbesseren."
"TaskOptimizeDatabaseDescription": "Kompriméiert dDatebank a schneit de fräie Speicherplatz zou. Dës Aufgab no engem Bibliothéik-Scan oder anere Ännerungen, déi Datebankmodifikatioune mat sech bréngen, auszeféieren, kann dPerformance verbesseren.",
"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."
}
@@ -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}",
@@ -65,8 +65,8 @@
"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",
@@ -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",
"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.",
"TaskMoveTrickplayImages": "Pakeisti Trickplay atvaizdų vietą",
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius Trickplay failus pagal bibliotekos nustatymus.",
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
"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 žodžių 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"
}
@@ -107,5 +107,6 @@
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām",
"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"
"Original": "Oriģināls",
"LyricDownloadFailureFromForItem": "Dziesmu vārdi nevarēja tikt lejupielādēti no {0} priekš {1}"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Opruimtaak gebruikersdata",
"Genres": "Genres",
"Original": "Oorspronkelijk",
"LyricDownloadFailureFromForItem": "Downloaden van liedteksten voor {1} van {0} mislukt"
"LyricDownloadFailureFromForItem": "Downloaden van liedteksten voor {1} van {0} mislukt",
"NameExtraBehindTheScenes": "Achter de schermen",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Geschrapte scène",
"NameExtraFeaturette": "Featurette",
"NameExtraInterview": "Vraaggesprek",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Voorbeeldfragment",
"NameExtraScene": "Scène",
"NameExtraShort": "Korte film",
"NameExtraThemeSong": "Themamuziek",
"NameExtraThemeVideo": "Themavideo",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra inhoud"
}
@@ -1,3 +1,27 @@
{
"AppDeviceValues": "Aplicacion: {0}, Periferic: {1}"
"AppDeviceValues": "Aplicacion: {0}, Periferic: {1}",
"Books": "Libres",
"Artists": "Artistas",
"Collections": "Collecciones",
"ChapterNameValue": "Capitol {0}",
"External": "Extèrn",
"Folders": "Dorsièrs",
"Favorites": "Favorits",
"HeaderContinueWatching": "Contunhar de regardar",
"HeaderFavoriteEpisodes": "Episòdis Favorits",
"AuthenticationSucceededWithUserName": "{0} autentificat amb succès",
"HeaderFavoriteShows": "Serias Favoritas",
"HeaderLiveTV": "TV en dirècte",
"HeaderNextUp": "Seguent",
"HearingImpaired": "Amb de deficiéncias auditivas",
"Movies": "Filmes",
"Music": "Musica",
"Latest": "Darrièr",
"Forced": "Forçat",
"Default": "Defaut",
"Genres": "Genres",
"HomeVideos": "Vidèos d'Acuèlh",
"Inherit": "Eiretar",
"LabelIpAddressValue": "Adreça IP: {0}",
"LabelRunningTimeValue": "Temps d'execucion : {0}"
}
@@ -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,17 @@
"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"
}
@@ -95,18 +95,30 @@
"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.",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskRefreshTrickplayImages": "Gerar imagens de Trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria miniaturas de pré-visualização (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.",
"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",
"TaskMoveTrickplayImages": "Migrar a localização das imagens de Trickplay",
"CleanupUserDataTask": "Task de limpeza de dados do usuário",
"CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}",
"NameExtraBehindTheScenes": "Bastidores",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Cena Eliminada",
"NameExtraFeaturette": "Média-metragem",
"NameExtraInterview": "Entrevista",
"NameExtraSample": "Amostra",
"NameExtraShort": "Curta-metragem",
"NameExtraThemeSong": "Tema Principal",
"NameExtraThemeVideo": "Vídeo de Abertura",
"NameExtraScene": "Cena",
"NameExtraUnknown": "Extra",
"NameExtraTrailer": "Trailer"
}
@@ -108,5 +108,16 @@
"CleanupUserDataTask": "Задача очистки пользовательских данных",
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней.",
"Original": "Оригинальный",
"LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}"
"LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}",
"NameExtraBehindTheScenes": "За кулисами",
"NameExtraClip": "Отрывок",
"NameExtraDeletedScene": "Удалённая сцена",
"NameExtraFeaturette": "Короткометражка",
"NameExtraInterview": "Интервью",
"NameExtraSample": "Образец",
"NameExtraScene": "Сцена",
"NameExtraThemeSong": "Заглавная песня",
"NameExtraThemeVideo": "Заглавное видео",
"NameExtraTrailer": "Трейлер",
"NameExtraUnknown": "Дополнительный материал"
}
@@ -1 +1,15 @@
{}
{
"AppDeviceValues": "ඇප්: {0}, උපාංග: {1}",
"Artists": "කලාකරුවන්",
"AuthenticationSucceededWithUserName": "{0} සාර්ථකව තහවුරු කරන ලදී",
"Books": "පොත්",
"ChapterNameValue": "{0} වෙනි පරිච්ඡේදය",
"Collections": "සංහිතා",
"Default": "පෙරනිමි",
"External": "බාහිර",
"FailedLoginAttemptWithUserName": "{0} වෙතින් සිදුකළ පිවිසීමේ උත්සාහය අසාර්ථක විය",
"Favorites": "ප්‍රියතමයන්",
"Folders": "ෆෝල්ඩර",
"Forced": "නියමිත",
"Genres": "ප්‍රභේද"
}
@@ -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"
}
@@ -106,5 +106,7 @@
"TaskAudioNormalization": "Normalizimi i audios",
"TaskAudioNormalizationDescription": "Skannon skedarët për të dhëna të normalizimit të audios.",
"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"
"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"
}
@@ -108,5 +108,13 @@
"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"
}
@@ -106,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment",
"TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay",
"CleanupUserDataTask": "ส่วนงานล้างข้อมูลผู้ใช้",
"CleanupUserDataTaskDescription": "ล้างข้อมูลผู้ใช้ทั้งหมด (สถานะการรับชม สถานะรายการโปรด ฯลฯ) จากสื่อที่ไม่ได้ใช้งานแล้วอย่างน้อย 90 วัน"
"CleanupUserDataTaskDescription": "ล้างข้อมูลผู้ใช้ทั้งหมด (สถานะการรับชม สถานะรายการโปรด ฯลฯ) จากสื่อที่ไม่ได้ใช้งานแล้วอย่างน้อย 90 วัน",
"LyricDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดเนื้อเพลงจาก {0} สำหรับ {1}",
"Original": "ต้นฉบับ"
}
@@ -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": "额外"
}
@@ -77,8 +77,8 @@
"TaskCleanLogsDescription": "自動刪走超過 {0} 日嘅紀錄檔。",
"TaskCleanLogs": "清理日誌資料夾",
"TaskRefreshLibrary": "掃描媒體櫃",
"TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節圖。",
"TaskRefreshChapterImages": "擷取章節圖",
"TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節預覽圖。",
"TaskRefreshChapterImages": "擷取章節圖",
"TaskCleanCacheDescription": "刪走系統已經唔再需要嘅快取檔案。",
"TaskCleanCache": "清理快取(Cache)資料夾",
"TasksChannelsCategory": "網路頻道",
@@ -106,5 +106,7 @@
"TaskMoveTrickplayImages": "遷移快轉縮圖位置",
"TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。",
"CleanupUserDataTask": "用戶資料清理工作",
"CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。"
"CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。",
"Original": "原作",
"LyricDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的歌詞"
}
@@ -261,6 +261,24 @@ namespace Emby.Server.Implementations.Localization
_cultures);
}
/// <inheritdoc />
public string? GetLanguageDisplayName(string language)
{
if (string.IsNullOrEmpty(language))
{
return null;
}
var displayName = FindLanguageInfo(language)?.DisplayName;
if (displayName is null)
{
return null;
}
// Truncate at the first delimiter to avoid cluttered display names
return displayName.Split([';', ','], StringSplitOptions.None)[0].Trim();
}
/// <inheritdoc />
public IReadOnlyList<CountryInfo> GetCountries()
{
@@ -356,6 +374,27 @@ namespace Emby.Server.Implementations.Localization
{
ArgumentException.ThrowIfNullOrEmpty(rating);
// Some providers may list multiple ratings separated by '/' (e.g. "SE:15 / SE:15+ / SE:Från 15 år").
// Try each one in order and use the first that resolves.
var ratingValues = rating.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var ratingValue in ratingValues)
{
var score = GetSingleRatingScore(ratingValue, countryCode);
if (score is not null)
{
return score;
}
}
return null;
}
/// <summary>
/// Resolves a single rating value to a score.
/// </summary>
private ParentalRatingScore? GetSingleRatingScore(string rating, string? countryCode)
{
// Handle unrated content
if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
@@ -364,7 +403,7 @@ namespace Emby.Server.Implementations.Localization
// Convert ints directly
// This may override some of the locale specific age ratings (but those always map to the same age)
if (int.TryParse(rating, out var ratingAge))
if (TryParseRatingAsScore(rating, out var ratingAge))
{
return new(ratingAge, null);
}
@@ -466,6 +505,13 @@ namespace Emby.Server.Implementations.Localization
return true;
}
// If it's not a recognized rating string, fall back to using the number as the score
if (TryParseRatingAsScore(ratingPart, out var numericScore))
{
result = new ParentalRatingScore(numericScore, null);
return true;
}
_logger.LogWarning(
"Rating '{Rating}' not found in the '{CountryCode}' rating system, treating as unrated",
rating,
@@ -480,6 +526,18 @@ namespace Emby.Server.Implementations.Localization
return true;
}
/// <summary>
/// Tries to parse a rating as a number, allowing an optional trailing '+' (e.g. "16" or "18+").
/// </summary>
/// <param name="ratingValue">Rating value to parse.</param>
/// <param name="score">Parsed score.</param>
/// <returns>Returns true if parsing was successful.</returns>
private static bool TryParseRatingAsScore(ReadOnlySpan<char> ratingValue, out int score)
{
var trimmed = ratingValue.TrimEnd('+');
return int.TryParse(trimmed, out score);
}
/// <inheritdoc />
public string GetLocalizedString(string phrase)
{
@@ -10,21 +10,42 @@
}
},
{
"ratingStrings": ["K12"],
"ratingStrings": ["K12", "K-12", "12"],
"ratingScore": {
"score": 12,
"subScore": null
}
},
{
"ratingStrings": ["K13", "K-13", "13"],
"ratingScore": {
"score": 13,
"subScore": null
}
},
{
"ratingStrings": ["K15"],
"ratingStrings": ["K15", "K-15", "15"],
"ratingScore": {
"score": 15,
"subScore": null
}
},
{
"ratingStrings": ["K18"],
"ratingStrings": ["K16", "K-16", "16"],
"ratingScore": {
"score": 16,
"subScore": null
}
},
{
"ratingStrings": ["K17", "K-17", "17"],
"ratingScore": {
"score": 17,
"subScore": null
}
},
{
"ratingStrings": ["K18", "K-18", "18", "18+"],
"ratingScore": {
"score": 18,
"subScore": null
@@ -10,7 +10,7 @@
}
},
{
"ratingStrings": ["7"],
"ratingStrings": ["7", "7+", "7 År", "Från 7 år"],
"ratingScore": {
"score": 7,
"subScore": null
@@ -31,7 +31,7 @@
}
},
{
"ratingStrings": ["11"],
"ratingStrings": ["11", "11+", "11 År", "Från 11 år"],
"ratingScore": {
"score": 11,
"subScore": null
@@ -45,11 +45,18 @@
}
},
{
"ratingStrings": ["15"],
"ratingStrings": ["15", "15+", "15 År", "Från 15 år"],
"ratingScore": {
"score": 15,
"subScore": null
}
},
{
"ratingStrings": ["18", "18+", "Barnförbjuden", "Bfj"],
"ratingScore": {
"score": 18,
"subScore": null
}
}
]
}
@@ -219,28 +219,15 @@ namespace Emby.Server.Implementations.Playlists
var playlist = _libraryManager.GetItemById(playlistId) as Playlist
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
// Retrieve all the items to be added to the playlist
// Retrieve all the items to be added to the playlist.
var newItems = GetPlaylistItems(newItemIds, user, options)
.Where(i => i.SupportsAddingToPlaylist);
// Filter out duplicate items
var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
newItems = newItems
.Where(i => !existingIds.Contains(i.Id))
.Distinct();
// Create a list of the new linked children to add to the playlist
var childrenToAdd = newItems
.Select(LinkedChild.Create)
.ToList();
// Log duplicates that have been ignored, if any
int numDuplicates = newItemIds.Count - childrenToAdd.Count;
if (numDuplicates > 0)
{
_logger.LogWarning("Ignored adding {DuplicateCount} duplicate items to playlist {PlaylistName}.", numDuplicates, playlist.Name);
}
// Do nothing else if there are no items to add to the playlist
if (childrenToAdd.Count == 0)
{
@@ -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);
}
@@ -632,9 +640,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 +772,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 +783,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 +905,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 +920,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;
}
}
}
@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@@ -234,7 +235,7 @@ public partial class AudioNormalizationTask : IScheduledTask
{
FileName = _mediaEncoder.EncoderPath,
Arguments = args,
RedirectStandardOutput = false,
StandardErrorEncoding = Encoding.UTF8,
RedirectStandardError = true
},
})
@@ -4,9 +4,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -21,7 +27,9 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
private readonly IFileSystem _fileSystem;
private readonly ILogger<PeopleValidationTask> _logger;
private readonly IItemTypeLookup _itemTypeLookup;
/// <summary>
/// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
@@ -29,13 +37,23 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <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>
public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbContextFactory, ILogger<PeopleValidationTask> logger)
/// <param name="itemTypeLookup">Instance of the <see cref="IItemTypeLookup"/> interface.</param>
public PeopleValidationTask(
ILibraryManager libraryManager,
ILocalizationManager localization,
IDbContextFactory<JellyfinDbContext> dbContextFactory,
IFileSystem fileSystem,
ILogger<PeopleValidationTask> logger,
IItemTypeLookup itemTypeLookup)
{
_libraryManager = libraryManager;
_localization = localization;
_dbContextFactory = dbContextFactory;
_fileSystem = fileSystem;
_logger = logger;
_itemTypeLookup = itemTypeLookup;
}
/// <inheritdoc />
@@ -83,10 +101,11 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
return;
}
// Phase 1: Deduplicate and remove orphaned people (0-33%)
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 3));
var dupQuery = context.Peoples
.GroupBy(e => new { e.Name, e.PersonType })
.Where(e => e.Count() > 1)
@@ -141,9 +160,104 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
subProgress.Report(100);
}
IProgress<double> validateProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
// 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);
// Phase 3: Refresh images for people missing them (66-100%)
IProgress<double> refreshProgress = new Progress<double>((val) => progress.Report((val / 3) + 66));
await RefreshPeopleImagesAsync(refreshProgress, cancellationToken).ConfigureAwait(false);
progress.Report(100);
}
private async Task RefreshPeopleImagesAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30);
var personTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
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
.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))
.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++;
}
numComplete++;
progress.Report(100.0 * numComplete / numPeople);
}
_logger.LogInformation("Refreshed metadata for {Count} people missing images or overview", numRefreshed);
}
}
private async Task<bool> RefreshPersonAsync(Guid personId, CancellationToken cancellationToken)
{
try
{
if (_libraryManager.GetItemById(personId) is not Person item)
{
return false;
}
var hasImage = item.HasImage(MediaBrowser.Model.Entities.ImageType.Primary);
var hasOverview = !string.IsNullOrEmpty(item.Overview);
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
ImageRefreshMode = hasImage ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default,
MetadataRefreshMode = hasOverview ? MetadataRefreshMode.ValidationOnly : MetadataRefreshMode.Default
};
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
return true;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing images for person {PersonId}", personId);
return false;
}
}
}
@@ -641,8 +641,7 @@ namespace Emby.Server.Implementations.Session
if (playingSessions.Count > 0)
{
var idle = playingSessions
.Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5)
.ToList();
.Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5);
foreach (var session in idle)
{
@@ -729,6 +728,31 @@ namespace Emby.Server.Implementations.Session
return item;
}
/// <summary>
/// Resolves the item whose user data (playback position, played status) should be updated
/// for a playback report. When an alternate version is played the client reports the displayed
/// item as <c>ItemId</c> and the played version as <c>MediaSourceId</c>.
/// </summary>
/// <param name="libraryItem">The now playing (displayed) item.</param>
/// <param name="mediaSourceId">The reported media source id.</param>
/// <returns>The item to track progress against.</returns>
private BaseItem GetProgressItem(BaseItem libraryItem, string mediaSourceId)
{
if (libraryItem is Video libraryVideo
&& !string.IsNullOrEmpty(mediaSourceId)
&& Guid.TryParse(mediaSourceId, out var mediaSourceItemId)
&& !mediaSourceItemId.Equals(libraryVideo.Id))
{
var versionItem = libraryVideo.GetAlternateVersion(mediaSourceItemId);
if (versionItem is not null)
{
return versionItem;
}
}
return libraryItem;
}
/// <summary>
/// Used to report that playback has started for an item.
/// </summary>
@@ -760,9 +784,10 @@ namespace Emby.Server.Implementations.Session
if (libraryItem is not null)
{
var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
foreach (var user in users)
{
OnPlaybackStart(user, libraryItem);
OnPlaybackStart(user, progressItem);
}
}
@@ -894,9 +919,10 @@ namespace Emby.Server.Implementations.Session
// only update saved user data on actual check-ins, not automated ones
if (libraryItem is not null && !isAutomated)
{
var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
foreach (var user in users)
{
OnPlaybackProgress(user, libraryItem, info);
OnPlaybackProgress(user, progressItem, info);
}
}
@@ -956,6 +982,20 @@ namespace Emby.Server.Implementations.Session
if (changed)
{
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None);
// A completed version marks every alternate version played and clears their resume points, so the
// whole movie leaves Continue Watching and reads as watched everywhere. (Per-version resume positions
// only persist while nothing has been completed yet.)
if (data.Played == true && item is Video playedVideo)
{
playedVideo.PropagatePlayedState(user, true);
}
}
if ((!user.RememberAudioSelections && info.AudioStreamIndex.HasValue)
|| (!user.RememberSubtitleSelections && info.SubtitleStreamIndex.HasValue))
{
_userDataManager.ResetPlaybackStreamSelections(user, item);
}
}
@@ -1087,9 +1127,10 @@ namespace Emby.Server.Implementations.Session
if (libraryItem is not null)
{
var progressItem = GetProgressItem(libraryItem, info.MediaSourceId);
foreach (var user in users)
{
playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed);
playedToCompletion = OnPlaybackStopped(user, progressItem, info.PositionTicks, info.Failed);
}
}
@@ -1142,6 +1183,14 @@ namespace Emby.Server.Implementations.Session
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
// A completed version marks every alternate version played and clears their resume points, so the
// whole movie leaves Continue Watching and reads as watched everywhere. (Per-version resume positions
// only persist while nothing has been completed yet.)
if (data.Played == true && item is Video playedVideo)
{
playedVideo.PropagatePlayedState(user, true);
}
return playedToCompletion;
}
@@ -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)
@@ -246,8 +246,21 @@ namespace Emby.Server.Implementations.Session
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
foreach (var webSocket in lost)
{
// TODO: handle session relative to the lost webSocket
RemoveWebSocket(webSocket);
// The connection stopped answering keep-alives, so a close frame will
// never arrive and the pending receive loop would hang forever, keeping
// the session (and e.g. its SyncPlay group membership) alive. Disposing
// the connection aborts the receive loop, which raises Closed and lets
// the session end normally.
try
{
webSocket.Dispose();
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Error disposing lost WebSocket from {RemoteEndPoint}.", webSocket.RemoteEndPoint);
}
}
}
}
@@ -3,34 +3,14 @@
using System;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
public class DateLastMediaAddedComparer : IUserBaseItemComparer
public class DateLastMediaAddedComparer : IBaseItemComparer
{
/// <summary>
/// Gets or sets the user.
/// </summary>
/// <value>The user.</value>
public User User { get; set; }
/// <summary>
/// Gets or sets the user manager.
/// </summary>
/// <value>The user manager.</value>
public IUserManager UserManager { get; set; }
/// <summary>
/// Gets or sets the user data manager.
/// </summary>
/// <value>The user data manager.</value>
public IUserDataManager UserDataManager { get; set; }
/// <summary>
/// Gets the name.
/// </summary>
@@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Sorting
return x.PremiereDate.Value;
}
if (x.ProductionYear.HasValue)
if (x.ProductionYear is not null)
{
try
{
@@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Sorting
return 0;
}
if (x.ProductionYear.HasValue)
if (x.ProductionYear is not null)
{
return x.ProductionYear.Value;
}
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
@@ -136,11 +137,15 @@ namespace Emby.Server.Implementations.TV
if (nextEpisode is not null)
{
// The last played date and the version that was actually played live on the version item's user data
// The played state propagated to the sibling versions carries no date
var (playedVersion, lastPlayedDate) = GetMostRecentlyPlayedVersion(result.LastWatched, user);
nextEpisode = GetPreferredVersion(nextEpisode, result.LastWatched, playedVersion);
DateTime lastWatchedDate = DateTime.MinValue;
if (result.LastWatched is not null)
{
var userData = _userDataManager.GetUserData(user, result.LastWatched);
lastWatchedDate = userData?.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
lastWatchedDate = lastPlayedDate ?? DateTime.MinValue.AddDays(1);
}
nextUpList.Add((lastWatchedDate, nextEpisode));
@@ -152,11 +157,13 @@ namespace Emby.Server.Implementations.TV
if (nextPlayedEpisode is not null)
{
var (playedVersion, lastPlayedDate) = GetMostRecentlyPlayedVersion(result.LastWatchedForRewatching, user);
nextPlayedEpisode = GetPreferredVersion(nextPlayedEpisode, result.LastWatchedForRewatching, playedVersion);
DateTime rewatchLastWatchedDate = DateTime.MinValue;
if (result.LastWatchedForRewatching is not null)
{
var userData = _userDataManager.GetUserData(user, result.LastWatchedForRewatching);
rewatchLastWatchedDate = userData?.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
rewatchLastWatchedDate = lastPlayedDate ?? DateTime.MinValue.AddDays(1);
}
nextUpList.Add((rewatchLastWatchedDate, nextPlayedEpisode));
@@ -219,10 +226,13 @@ namespace Emby.Server.Implementations.TV
if (nextEpisode is not null && !includeResumable)
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
if (userData?.PlaybackPositionTicks > 0)
// The resume progress may live on an alternate version
foreach (var version in nextEpisode.GetAllVersions())
{
return null;
if (_userDataManager.GetUserData(user, version)?.PlaybackPositionTicks > 0)
{
return null;
}
}
}
@@ -237,6 +247,74 @@ namespace Emby.Server.Implementations.TV
return DetermineNextEpisode(result, user, includeSpecials, includeResumable: false, includePlayed: true);
}
/// <summary>
/// Gets the version of the last watched episode that was actually played, together with its last played date.
/// The version that was played carries the most recent LastPlayedDate.
/// dates.
/// </summary>
/// <param name="lastWatched">The last watched episode (any version).</param>
/// <param name="user">The user.</param>
/// <returns>The played version and its last played date.</returns>
private (Video? PlayedVersion, DateTime? LastPlayedDate) GetMostRecentlyPlayedVersion(BaseItem? lastWatched, User user)
{
if (lastWatched is not Video lastWatchedVideo)
{
return (null, null);
}
var versions = lastWatchedVideo.GetAllVersions();
var userDataByVersion = _userDataManager.GetUserDataBatch(versions, user);
var playedVersion = VersionPlaybackSelector.SelectMostRecentlyPlayed(
versions,
version => userDataByVersion.GetValueOrDefault(version.Id),
data => data.LastPlayedDate.HasValue);
return (playedVersion, playedVersion is null ? null : userDataByVersion[playedVersion.Id].LastPlayedDate);
}
/// <summary>
/// When the last watched episode was played as an alternate version, prefer the next episode's version with the matching name,
/// so Next Up continues in the version the user has been watching instead of falling back to the primary.
/// </summary>
/// <param name="nextEpisode">The determined next episode (a primary).</param>
/// <param name="lastWatched">The last watched episode.</param>
/// <param name="playedVersion">The version of the last watched episode that was played.</param>
/// <returns>The matching version of the next episode, or the episode itself.</returns>
private Episode GetPreferredVersion(Episode nextEpisode, BaseItem? lastWatched, Video? playedVersion)
{
// No version preference, or the primary was played
if (lastWatched is not Video lastWatchedVideo
|| playedVersion is null
|| !playedVersion.PrimaryVersionId.HasValue)
{
return nextEpisode;
}
// Match by version name
var playedVersionId = playedVersion.Id.ToString("N", CultureInfo.InvariantCulture);
var playedVersionName = lastWatchedVideo.GetMediaSources(false)
.FirstOrDefault(source => string.Equals(source.Id, playedVersionId, StringComparison.OrdinalIgnoreCase))?.Name;
if (string.IsNullOrEmpty(playedVersionName))
{
return nextEpisode;
}
var matchingSource = nextEpisode.GetMediaSources(false)
.FirstOrDefault(source => string.Equals(source.Name, playedVersionName, StringComparison.OrdinalIgnoreCase));
if (matchingSource is not null
&& Guid.TryParse(matchingSource.Id, out var matchingId)
&& !matchingId.Equals(nextEpisode.Id)
&& _libraryManager.GetItemById<Episode>(matchingId) is { } matchingVersion)
{
return matchingVersion;
}
return nextEpisode;
}
private static string GetUniqueSeriesKey(Series series)
{
return series.GetPresentationUniqueKey();
@@ -500,8 +500,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;
}
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Queries;
using Jellyfin.Database.Implementations.Enums;
@@ -84,36 +84,9 @@ public class ActivityLogController : BaseJellyfinApiController
ItemId = itemId,
Username = username,
Severity = severity,
OrderBy = GetOrderBy(sortBy ?? [], sortOrder ?? []),
OrderBy = RequestHelpers.GetOrderBy(sortBy ?? [], sortOrder ?? []),
};
return await _activityManager.GetPagedResultAsync(query).ConfigureAwait(false);
}
private static (ActivityLogSortBy SortBy, SortOrder SortOrder)[] GetOrderBy(
IReadOnlyList<ActivityLogSortBy> sortBy,
IReadOnlyList<SortOrder> requestedSortOrder)
{
if (sortBy.Count == 0)
{
return [];
}
var result = new (ActivityLogSortBy, SortOrder)[sortBy.Count];
var i = 0;
for (; i < requestedSortOrder.Count; i++)
{
result[i] = (sortBy[i], requestedSortOrder[i]);
}
// Add remaining elements with the first specified SortOrder
// or the default one if no SortOrders are specified
var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
for (; i < sortBy.Count; i++)
{
result[i] = (sortBy[i], order);
}
return result;
}
}
@@ -1456,22 +1456,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 +1499,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 +1537,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)
@@ -1607,8 +1612,9 @@ public class DynamicHlsController : BaseJellyfinApiController
if (state.VideoStream is not null && state.IsOutputVideo)
{
// fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
// fMP4 needs frag_discont to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
// HLS does not use SIDX, and skipping it avoids FFmpeg rewriting open-GOP boundary packet PTS
hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont+skip_sidx";
}
segmentFormat = "fmp4" + outputFmp4HeaderArg;
+37 -4
View File
@@ -158,13 +158,43 @@ public class FilterController : BaseJellyfinApiController
IsSeries = isSeries
};
var streamLanguageQuery = new InternalItemsQuery(user)
{
// It's possible that different langauges are only available on alternative versions.
// To fetch them all, owned items are included.
IncludeOwnedItems = true,
IncludeItemTypes = includeItemTypes,
DtoOptions = new DtoOptions
{
Fields = Array.Empty<ItemFields>(),
EnableImages = false,
EnableUserData = false
},
IsAiring = isAiring,
IsMovie = isMovie,
IsSports = isSports,
IsKids = isKids,
IsNews = isNews,
IsSeries = isSeries
};
if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
{
genreQuery.AncestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
var ancestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
genreQuery.AncestorIds = ancestorIds;
streamLanguageQuery.AncestorIds = ancestorIds;
}
else
{
genreQuery.Parent = parentItem;
streamLanguageQuery.Parent = parentItem;
}
if ((includeItemTypes.Contains(BaseItemKind.Series) || includeItemTypes.Contains(BaseItemKind.Season))
&& !includeItemTypes.Contains(BaseItemKind.Episode))
{
// streams are joined on epsiodes not shows or seasons
streamLanguageQuery.IncludeItemTypes = [.. includeItemTypes, BaseItemKind.Episode];
}
if (includeItemTypes.Length == 1
@@ -188,10 +218,13 @@ public class FilterController : BaseJellyfinApiController
}).ToArray();
}
if (includeItemTypes.Contains(BaseItemKind.Movie) || includeItemTypes.Contains(BaseItemKind.Series))
if (includeItemTypes.Contains(BaseItemKind.Movie)
|| includeItemTypes.Contains(BaseItemKind.Series)
|| includeItemTypes.Contains(BaseItemKind.Season)
|| includeItemTypes.Contains(BaseItemKind.Episode))
{
filters.AudioLanguages = _libraryManager
.GetMediaStreamLanguages(MediaStreamType.Audio)
.GetMediaStreamLanguages(MediaStreamType.Audio, streamLanguageQuery)
.Select(language =>
{
var culture = _localization.FindLanguageInfo(language);
@@ -204,7 +237,7 @@ public class FilterController : BaseJellyfinApiController
.OrderBy(l => l.Name)
.ToArray();
filters.SubtitleLanguages = _libraryManager
.GetMediaStreamLanguages(MediaStreamType.Subtitle)
.GetMediaStreamLanguages(MediaStreamType.Subtitle, streamLanguageQuery)
.Select(language =>
{
var culture = _localization.FindLanguageInfo(language);
@@ -60,11 +60,8 @@ public class HlsSegmentController : BaseJellyfinApiController
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
// TODO: Deprecate with new iOS app
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
var file = ValidateTranscodePath(string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())));
if (file is null)
{
return BadRequest("Invalid segment.");
}
@@ -86,12 +83,9 @@ public class HlsSegmentController : BaseJellyfinApiController
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
|| Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
var file = ValidateTranscodePath(string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan())));
if (file is null
|| !Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Invalid segment.");
}
@@ -140,18 +134,13 @@ public class HlsSegmentController : BaseJellyfinApiController
[FromRoute, Required] string segmentId,
[FromRoute, Required] string segmentContainer)
{
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
var file = ValidateTranscodePath(string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())));
if (file is null)
{
return BadRequest("Invalid segment.");
}
var normalizedPlaylistId = playlistId;
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
// Add . to start of segment container for future use.
segmentContainer = segmentContainer.Insert(0, ".");
@@ -161,7 +150,7 @@ public class HlsSegmentController : BaseJellyfinApiController
var pathExtension = Path.GetExtension(path);
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
&& path.Contains(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase))
&& path.Contains(playlistId, StringComparison.OrdinalIgnoreCase))
{
playlistPath = path;
break;
@@ -173,6 +162,19 @@ public class HlsSegmentController : BaseJellyfinApiController
: GetFileResult(file, playlistPath);
}
private string? ValidateTranscodePath(string filename)
{
var transcodePath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(_serverConfigurationManager.GetTranscodePath()));
var file = Path.GetFullPath(filename, transcodePath);
// Require a separator after the transcode path so a sibling like "<transcodePath>-evil" can't pass.
if (!file.StartsWith(transcodePath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
{
return null;
}
return file;
}
private ActionResult GetFileResult(string path, string playlistPath)
{
var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+8 -2
View File
@@ -125,7 +125,13 @@ public class ImageController : BaseJellyfinApiController
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
var userConfigurationDirectoryPath = _serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath;
var userDataPath = Path.Combine(userConfigurationDirectoryPath, user.Username);
if (!PathHelper.IsContainedIn(userConfigurationDirectoryPath, userDataPath))
{
return BadRequest("Invalid user.");
}
if (user.ProfileImage is not null)
{
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
@@ -2049,7 +2055,7 @@ public class ImageController : BaseJellyfinApiController
}
// Check If-Modified-Since header for time-based validation
if (DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader))
if (DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], CultureInfo.InvariantCulture, out var ifModifiedSinceHeader))
{
// Return 304 if the image has not been modified since the client's cached version
if (dateImageModified <= ifModifiedSinceHeader)

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