Compare commits

..

1037 Commits

Author SHA1 Message Date
benvin ec581b5e5f Merge pull request 'fix(ha): gate library tasks on the scan leader by default' (#11) from benvin/scan-leader-default into main
ci/woodpecker/push/ci Pipeline was successful
Reviewed-on: #11
2026-09-13 15:04:38 +10:00
unkin-agent b77702c851 merge: resolve CoreAppHost conflict with main, drop dead redisConnectionString local
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
2026-09-13 14:48:26 +10:00
benvin 2370787471 Merge pull request 'fix(db): collapse presentation-key groups without min(uuid)' (#10) from benvin/fix-guid-aggregate-pg into main
ci/woodpecker/push/ci Pipeline was successful
Reviewed-on: #10
2026-09-13 14:25:19 +10:00
benvin a652d718ca Merge pull request 'fix(ha): read transcode store config from the variable form deployments set' (#9) from benvin/ha-config-wiring into main
ci/woodpecker/push/ci Pipeline was canceled
Reviewed-on: #9
2026-09-13 14:24:11 +10:00
benvin 0c707c7989 Merge pull request 'fix(devices): read devices and device options through to the database' (#8) from benvin/fix-device-cache into main
ci/woodpecker/push/ci Pipeline was canceled
Reviewed-on: #8
2026-09-13 14:23:17 +10:00
unkin-agent 91655fcb0a fix(ha): gate library tasks on the scan leader by default
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Jellyfin:ScanLeader:Enabled defaults to false and nothing sets it, so
leader election never runs and every replica executes the timer-driven
library tasks concurrently - the exact behaviour the lease prevents.

- Enable gating by default when a Redis connection string is configured
- Honour an explicit Enabled setting either way
- Carry the effective decision onto the bound options the task worker reads
- Log at startup whether gating is active
- Warn when gating is enabled but no Redis connection string is configured
2026-09-13 13:19:48 +10:00
unkin-agent 6e405af1dc fix(db): collapse presentation-key groups without min(uuid)
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
PostgreSQL has no min(uuid) aggregate, so every query that picked a group
representative with MIN over the item id failed with 42883: library browse,
search, Recently Added, the by-name endpoints and Upcoming.

- Pick the representative with an anti-join on (primary version, id)
- Cover the collapse with a repository test against a real PostgreSQL
2026-09-13 13:18:11 +10:00
unkin-agent 95220e2ed6 fix(ha): read transcode store config from the variable form deployments set
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The startup configuration only reads JELLYFIN_ prefixed environment
variables, so the bare Jellyfin__TranscodeStore__* form used by the chart,
the manifests and the README is dropped and the Redis store is never
registered. Nothing logs the selected store, so the fallback is invisible.

- Read bare Jellyfin__* variables into the Jellyfin:* configuration root
- Keep an explicit JELLYFIN_ variable winning over the bare form
- Log the selected transcode session store at startup
- Ping Redis once at startup and log an unreachable store at Error
- Log endpoints only, never the connection string
2026-09-13 13:10:52 +10:00
unkin-agent 56919b9565 fix(devices): read devices and device options through to the database
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The device cache was filled once at construction, so a token minted by one
replica was unknown to every other replica already running and a token revoked
on one replica stayed valid on the others until they restarted.

- drop the eager device and device options dictionaries
- read devices and device options from the database on every query
- push the device query filters and ordering into SQL
- open the request's database context only for the api key fallback
- cover both directions against real PostgreSQL with two manager instances
2026-09-13 13:08:47 +10:00
benvin 25269263ce Merge pull request 'test(db): run the startup migration chain against PostgreSQL in CI' (#7) from benvin/ci-postgres-migration-chain into main
ci/woodpecker/push/ci Pipeline was successful
Reviewed-on: #7
2026-09-12 22:22:12 +10:00
benvin b183bec94b Merge pull request 'fix(db): materialise the rating list before updating' (#6) from benvin/fix-rating-levels-pg into main
ci/woodpecker/push/ci Pipeline was canceled
Reviewed-on: #6
2026-09-12 22:21:30 +10:00
unkin-agent bc42ff4b28 test(db): drop a leftover test database before recreating it
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
A server handed in through JELLYFIN_TEST_POSTGRES outlives the run, so a second
run finds the databases the first one created. Also name the failures in
Jellyfin.Database.Tests.PostgreSQL the CI step steps around.
2026-09-12 21:36:09 +10:00
unkin-agent b501ba9620 test(db): run the startup migration chain against PostgreSQL in CI
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Every PostgreSQL test carries Category=RequiresDocker and the pipeline filters
that category out, so the provider production runs on was never exercised and
two upgrade-path bugs reached it in a row. Nothing covered the whole startup
sequence either - code and schema migrations interleaved the way the migration
service orders them.

- run both database stages through JellyfinMigrationService against a seeded
  library and against a fresh install
- take the server from JELLYFIN_TEST_POSTGRES when it is set, else start a
  container
- add a pipeline step that runs the PostgreSQL tests on every push and pull
  request, with the server inside the step: the kubernetes backend has no
  docker daemon, and a service container deadlocks on the workspace volume
2026-09-12 21:28:17 +10:00
unkin-agent acab85ada3 merge the rating levels fix so the chain test has it 2026-09-12 21:20:27 +10:00
unkin-agent ad834167d5 ci: probe service containers
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/probe Pipeline was canceled
2026-09-12 21:03:15 +10:00
unkin-agent f59ab11c43 fix(db): materialise the rating list before updating
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
MigrateRatingLevels issued an ExecuteUpdate while the SELECT DISTINCT reader
was still open on the same connection. SQLite tolerates that, Npgsql does not,
so the AppInitialisation stage aborted and every PostgreSQL instance
crash-looped on startup.

- read the distinct ratings into a list before the update loop
- cover the migration against a real PostgreSQL, with NULL and empty ratings
2026-09-12 21:00:18 +10:00
benvin 10fed9409d Merge pull request 'fix(db): restore the PostgreSQL upgrade path' (#4) from benvin/fix-postgres-upgrade into main
ci/woodpecker/push/ci Pipeline was successful
Reviewed-on: #4
2026-09-12 17:55:56 +10:00
benvin f2166df463 Merge pull request 'fix(config): read an unusable encoding.xml EncoderPreset as the default' (#5) from benvin/fix-encoder-preset-fallback into main
ci/woodpecker/push/ci Pipeline was successful
Reviewed-on: #5
2026-09-12 17:43:53 +10:00
unkin-agent 3b60289502 fix(config): read an unusable encoding.xml EncoderPreset as the default
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
An element the enum cannot parse throws, and the configuration manager
catches that and returns defaults, so one bad preset discarded every other
encoding setting.

- serialize EncoderPreset through a string surrogate in XML
- fall back to auto for an empty, unknown or out-of-range value
2026-09-12 16:26:01 +10:00
unkin-agent 2f9b8888e9 revert: move the encoding.xml EncoderPreset fix to its own branch
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
It is an unrelated subsystem and ships as a separate change.
2026-09-12 16:21:47 +10:00
unkin-agent 65af2bcbd7 fix(db): harden the PostgreSQL upgrade against odd legacy data
- accept only the two id shapes the uuid cast parses, so misplaced hyphens
  null out instead of aborting the migration
- clear an OwnerId that already is the detached placeholder, matching the
  SQLite chain, so CleanupOrphanedExtras cannot delete the item
- recreate the placeholder item before the repoint if it is missing
- compare the migrated schema against the model, constraints included
2026-09-12 16:21:42 +10:00
unkin-agent 2e3d24cc05 fix(db): restore the PostgreSQL upgrade path
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The v12.0 rebase replaced the PostgreSQL provider's initial migration instead of
adding to it, so an existing database kept the pre-12.0 schema while the code
migrations ran against it and startup aborted on a missing NormalizedUsername.

- restore 20260305010333_InitialPostgreSql as the baseline
- add 20260306000000_UpgradeToServer12Schema carrying it to the 12.0 model
- convert OwnerId and PrimaryVersionId to uuid with explicit casts, clear
  unparseable ids and repoint dangling owners at the placeholder item
- drop orphaned permissions and preferences before UserId becomes non-nullable
- add 20260524120336_AddUniqueNormalizedUsernameIndex after the code migration
  that fills the column in
- read an empty or unknown encoding.xml EncoderPreset as the default
- cover both paths against a real PostgreSQL and guard the migration ordering
2026-09-12 15:32:12 +10:00
benvin dcb6ae396c Merge pull request 'Re-apply HA fork patches on upstream v12.0' (#3) from benvin/rebase-v12 into main
ci/woodpecker/push/ci Pipeline was successful
Reviewed-on: #3
2026-09-12 12:48:35 +10:00
unkin-agent c438ec17ed feat(helm): expose the transcode session retention window
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The chart sets the lease duration but not the retention window that keeps an
orphaned session record available for takeover.

- add ha.transcodeStore.sessionRetentionSeconds and pass it to the statefulset
2026-09-12 12:43:46 +10:00
unkin-agent f54bc76767 chore: merge main history into the v12.0 rebase
The branch is a fresh rebase onto the v12.0 tag, so it shares no history with
main and cannot be merged normally.

- record main as a parent without changing a single file
2026-09-12 10:19:36 +10:00
unkin-agent baa16b6586 fix: make transcode leases ownership-checked and cleanup-aware
Cleanup never matched a live session because the controller registered empty
manifest and segment paths, renewal was a read-modify-write that could revert a
takeover, and the takeover script compared an ISO date to a number, so it errored.

- populate the session record's manifest and segment paths from the playlist path
- renew the lease via a Lua compare-and-set on the owning pod
- store the lease expiry as unix milliseconds so the scripts can compare it
- retain the session record past its lease so an orphan can still be taken over
- test the Redis store against a real Redis, including the renew-vs-takeover race
- drop the live stream record nothing ever read back
2026-09-12 10:19:29 +10:00
unkin-agent d825f8ac81 ci: keep the package cache off the workspace volume
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The workspace volume is 9.8G and the clone plus build output already fill 5.7G,
so a shared package cache there leaves too little free space to test against.

- restore, build and test in one step with the cache on ephemeral storage
- exclude BackupServiceTests, which requires 5GiB free on the workspace
- print workspace free space before the test run
2026-09-12 01:03:59 +10:00
unkin-agent 0148f374ee ci: grant ephemeral storage and harden the package install
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
The backup service test needs 5GiB free on the workspace and the apt mirror
occasionally serves a half-synced index, so both fail the pipeline at random.

- request and limit ephemeral storage on every step
- retry apt-get update and verify fontconfig is loadable before testing
- report workspace free space before the test run
- run the full test filter again
2026-09-12 00:57:51 +10:00
unkin-agent d8c7eb5d3d ci: skip the backup service test on the pipeline workspace
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline failed
BackupService refuses to write a backup with less than 5GiB free, which the
pipeline workspace volume does not have, so the suite fails on disk size alone.

- exclude BackupServiceTests from the test filter
- keep restore, build and test as separate steps
- install fontconfig for the Skia tests before running them
2026-09-12 00:51:47 +10:00
unkin-agent 03ac3bbc00 ci: run each test suite as its own step
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
A solution-wide test run reports one exit code, so a failing suite cannot be
identified without the pipeline logs.

- run the suites carrying HA tests as separate named steps
- run the remaining upstream suites in one step after installing fontconfig
2026-09-12 00:44:47 +10:00
unkin-agent e235673f12 ci: probe system packages before building
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
A failure anywhere in the chain currently looks the same from outside, so there
is no way to tell a package mirror problem from a compile error.

- run the fontconfig install as the first step
- split restore, build and test into named steps
- point the package cache at the shared pipeline volume
2026-09-12 00:35:03 +10:00
unkin-agent b6f7732f13 ci: run the pipeline as one step again
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
Separate steps do not share the SDK package cache, so the build step could not
see what the restore step downloaded.

- restore, build and test in a single container
- install fontconfig after the build so a mirror failure cannot be read as a build failure
- raise the memory limit to 8Gi
2026-09-12 00:27:48 +10:00
unkin-agent 43b4f143eb ci: split the pipeline into restore, build and test steps
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
A single step reports every failure the same way, so a mirror, compiler or test
failure all look identical from the outside.

- split restore, build and test into separate named steps
- probe the fontconfig package install in its own step
- share the NuGet cache through the workspace and raise the memory limit
2026-09-12 00:24:26 +10:00
unkin-agent 9b8748c778 docs: document the HA layer and the v12.0 fork base
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
The README and fork notes still described upstream and a pre-10.11 snapshot, so
there was nothing accurate to hand an operator setting this up.

- rewrite the README as an HA setup and configuration guide
- add architecture, contributing and transcoding design notes
- rewrite FORK-DIFF.md against the v12.0 base
2026-09-11 23:58:18 +10:00
unkin-agent 673d186de8 feat(deploy): add jellyfin-ha Helm chart
Running the HA build needs Redis, PostgreSQL and shared transcode storage wired
together, which is a lot of manifests to keep in sync by hand.

- add a chart deploying the server as a StatefulSet with a PodDisruptionBudget
- ship optional Redis and PostgreSQL dependencies and a runtime config ConfigMap
- template shared media, config and transcode volume claims
- add ingress and ServiceMonitor templates
2026-09-11 23:56:51 +10:00
unkin-agent 14d843897c ci: build on Woodpecker and drop GitHub Actions
The fork is hosted on Gitea, where the upstream GitHub Actions workflows never
run and only produce noise.

- add a Woodpecker pipeline restoring, building and testing the solution
- remove the upstream .github/workflows definitions
- add Dockerfile and Dockerfile.runtime building the server image with jellyfin-web 12.0
2026-09-11 23:56:51 +10:00
unkin-agent 3b416c7fb2 feat: gate periodic library tasks behind a scan-leader lease
Timer-driven library tasks fire on every replica, so a library refresh or a
database optimise runs once per pod against the same shared library.

- add IScanLeaderLease with a Redis TTL implementation and a no-op default
- skip timer-driven runs of the gated tasks on instances without the lease
- treat an unreachable Redis as holding the lease so tasks never stop running
- leave manual and API-triggered runs ungated
2026-09-11 23:56:51 +10:00
unkin-agent e3571eb2a4 feat(session): record open live streams in the session store
Live stream ownership is only tracked in process memory, so no replica can tell
which pod holds a stream open once that pod is gone.

- persist a LiveStreamSession record when the live stream mappings change
- delete the record when the stream is closed
- keep closing the stream when the store is unreachable
2026-09-11 23:56:35 +10:00
unkin-agent 6ccabd4c72 feat(hls): register transcode sessions and renew their lease
A transcode that is not in the store is invisible to the other replicas, so the
HLS entry points have to publish and hold the lease themselves.

- register the play session in the store when ffmpeg starts
- renew the lease on a background loop and delete the session when it ends
- shorten segments and bound the playlist window when resuming a stored session
- add RecoverySegmentLengthSeconds and RecoverySegmentBufferCount encoding options
2026-09-11 23:56:35 +10:00
unkin-agent c43992630e feat: skip transcode files held by an active session
Every replica runs the transcode cleanup task against the same shared transcode
directory, so one pod deletes segments another pod is still streaming.

- read the active sessions from ITranscodeSessionStore before deleting
- keep files matching an active session manifest path or segment prefix
- abort the sweep when the store cannot be reached
2026-09-11 23:56:35 +10:00
unkin-agent e23d1a23be feat: add a durable transcode session store
Transcode state lives only in the process that started ffmpeg, so a pod restart
drops every in-flight HLS stream with no way for a peer to pick it up.

- add ITranscodeSessionStore plus the TranscodeSession and LiveStreamSession records
- add RedisTranscodeSessionStore with TTL leases and an atomic Lua takeover script
- add NullTranscodeSessionStore for single-instance deployments
- pick the store from Jellyfin:TranscodeStore:RedisConnectionString at startup
2026-09-11 23:56:19 +10:00
unkin-agent c8a693ae17 feat(tools): add SQLite to PostgreSQL migration tool
Existing installs hold their whole library in jellyfin.db, so switching to the
PostgreSQL provider needs a one-shot data move.

- add Jellyfin.DbMigrator reading SQLite tables and bulk-writing them to PostgreSQL
- validate table names against the source schema before generating SQL
- upload a pre-migration copy of the SQLite file to S3 when S3_BACKUP_BUCKET is set
- emit a per-table row-count report and support a dry-run mode
2026-09-11 23:56:19 +10:00
unkin-agent e68455f0ed feat(db): add opt-in PostgreSQL database provider
Multi-replica deployments cannot share a SQLite file, so the database has to
move to a server engine before the rest of the HA work is usable.

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

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

Fixes #17683.

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

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

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

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

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

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

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

* Make UserId non-nullable

* Remove unnecessary ToList

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

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

---------

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2026-08-20 18:35:26 +02:00
Oggeb1 c15656280e Merge branch 'jellyfin:master' into BDMV-pgs 2026-08-20 10:56:35 +02:00
Jacky He c70933a23f Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/
2026-08-20 02:51:23 +00:00
Jacky He ad9056c8fc Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/
2026-08-20 02:51:22 +00:00
Jacky He 77988e51b6 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/
2026-08-20 02:51:21 +00:00
theguymadmax 80fe8c67e9 Fix latest items for mixed libraries 2026-08-19 18:34:21 -04:00
Blackspirits 4029de92a1 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2026-08-19 22:08:08 +00:00
krvi 19d919f8be Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-19 16:33:14 +00:00
Blackspirits ef01d88912 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-19 16:33:14 +00:00
Shadowghost bd085665d6 Merge remote-tracking branch 'upstream/master' into safeguard-invalid-provider-ids 2026-08-19 16:13:29 +02:00
Ömür Ege Kiraz f586fc063f Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2026-08-19 13:29:15 +00:00
Oskar Bali 0b20d7a05b Fix BDMV PGS subtitles with TrueHD
Add myself CONTRIBUTORS.md
2026-08-19 12:19:32 +02:00
MakeMike 17221057bc Translated using Weblate (Armenian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hy/
2026-08-19 08:00:16 +00:00
Cody Robibero 04b77ca801 Merge pull request #17670 from Shadowghost/harden-musicbrainz
More resilient MusicBrainz lookup
2026-08-18 19:28:22 -04:00
Blackspirits b11734d955 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-18 22:07:07 +00:00
Cody Robibero 4a659fb30c Merge pull request #17631 from itsb/fix/confirmed-resume-position-master
Use client-reported position for idle playback cleanup
2026-08-18 17:12:00 -04:00
MoonsvnLyn c203da245d Fix Cheesegeezer GitHub URL typo in CONTRIBUTORS (#17659) 2026-08-18 22:36:32 +02:00
deepak r 0f36c4a200 Translated using Weblate (Tamil)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ta/
2026-08-18 18:36:57 +00:00
Jonathan Biton c4dec76b9c Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he/
2026-08-18 18:36:57 +00:00
m0g3r 46be43ad24 Prevent orphaned user permissions and preferences (#17643)
Prevent orphaned user permissions and preferences
2026-08-18 18:16:48 +02:00
Bond-009 3fe35fcbc1 Merge pull request #17665 from jellyfin/renovate/microsoft
Update Microsoft to 5.9.0
2026-08-18 17:29:29 +02:00
Shadowghost 43cdbe856a More resilient MusicBrainz lookup 2026-08-18 11:07:31 +02:00
Cody Robibero 72405bb146 Merge pull request #17658 from martin-77/fix/child-count-sql-variable-limit
Fix SQLite variable limit in child count batches
2026-08-17 18:18:29 -04:00
renovate[bot] 2bb281ad4c Update Microsoft to 5.9.0 2026-08-17 19:07:58 +00:00
aleksantero a2ab2fe340 Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/
2026-08-17 17:56:58 +00:00
Blackspirits 7df3c09760 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2026-08-17 08:10:11 +00:00
Blackspirits d26d3f4967 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-17 08:10:10 +00:00
Blackspirits 8718c00a5d Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2026-08-17 07:55:04 +00:00
Blackspirits 0c68b190bc Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-17 07:55:04 +00:00
Thadah D. Denyse 2c26ae6724 Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eu/
2026-08-17 07:49:17 +00:00
Emanuel Lopes 300c277816 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-17 07:49:17 +00:00
nenadsuperzmaj 69b21ddc0e Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sr/
2026-08-17 07:16:19 +00:00
Gallyam Biktashev 90a0e9d962 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/
2026-08-17 07:16:18 +00:00
martin-77 c539ee7e81 Fix SQLite variable limit in child count batches 2026-08-17 01:43:10 +02:00
Cody Robibero f97b7b120d Merge pull request #17655 from martin-77/fix/playlist-sql-variable-limits
Fix large playlist persistence with WhereOneOrMany
2026-08-16 18:53:43 -04:00
martin-77 7c148c3c7c Fix large playlist persistence 2026-08-16 21:30:37 +02:00
Bond-009 4fe980f25b Merge pull request #17614 from jellyfin/renovate/dotnet-monorepo
Update dependency dotnet-ef to v10.0.11
2026-08-16 16:51:32 +02:00
itsb f0c5370e27 Preserve unknown idle playback position 2026-08-16 09:09:18 -05:00
Andrea Baitelli 5f14c12c89 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-08-16 13:18:01 +00:00
Andrea Baitelli 49c78d4624 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-08-16 13:13:05 +00:00
Pedro Fonseca 1b698db422 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-08-16 12:59:23 +00:00
Bond-009 97d3e2cba6 Merge pull request #17638 from jellyfin/renovate/microsoft
Update dependency Microsoft.NET.Test.Sdk to 18.9.0
2026-08-16 14:19:31 +02:00
Bond-009 4680d00d2e Merge pull request #17630 from jellyfin/renovate/ci-deps
Update github/codeql-action action to v4.37.7
2026-08-16 13:48:16 +02:00
Ulrik 30c2603911 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/
2026-08-16 04:22:54 +00:00
Vitalijus a74246a000 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-15 20:59:10 +00:00
KecskeTech 1e04187b52 Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/
2026-08-15 16:36:20 +00:00
itsb 499e64b1c3 Use client-reported position for idle playback cleanup 2026-08-14 23:31:39 -05:00
krvi 916c3c9cc3 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-15 01:00:08 +00:00
VeryUsual f70e0563ee Translated using Weblate (Esperanto)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eo/
2026-08-15 01:00:07 +00:00
AlaronAndes d2f517a858 Translated using Weblate (Albanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sq/
2026-08-15 01:00:07 +00:00
dodog 0ae4a70e1c Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/
2026-08-15 01:00:06 +00:00
Shadowghost 4de43d36dd Require session ownership for additional users, capabilities and viewing reports 2026-08-14 14:50:07 +02:00
Shadowghost b1e3cf1341 Key active sessions by user to prevent session takeover 2026-08-14 14:50:06 +02:00
Shadowghost dd7de41878 Enforce EnableRemoteControlOfOtherUsers on session control 2026-08-14 14:50:06 +02:00
Cody Robibero e80fc120bf Merge pull request #17636 from st7105/fix/clean-web-dl-release-tag
Recognize WEB-DL release tags in video names
2026-08-14 08:29:37 -04:00
Cody Robibero 6982ba99ce Merge branch 'master' into fix/clean-web-dl-release-tag 2026-08-14 07:11:04 -04:00
renovate[bot] 8cb7297e27 Update dependency Microsoft.NET.Test.Sdk to 18.9.0 2026-08-14 11:07:01 +00:00
renovate[bot] 95d95de6f1 Update github/codeql-action action to v4.37.7 2026-08-14 11:06:54 +00:00
renovate[bot] 7704346e9c Update dependency dotnet-ef to v10.0.11 2026-08-14 11:06:49 +00:00
Cody Robibero 6a021d5dae Merge pull request #17637 from IDisposable/fix/de-localization-test-error
Fix other two unit test for localization of Artists in DE
2026-08-14 07:05:50 -04:00
José Tojeiro 6ebe66c19d Translated using Weblate (Galician)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gl/
2026-08-14 10:02:59 +00:00
Shadowghost 40a449c6f2 Count alternate versions in the media filters and align filter conditions 2026-08-14 08:46:21 +02:00
Shadowghost 06e37a7bf6 Merge remote-tracking branch 'upstream/master' into optimize-db-helper-memory 2026-08-14 08:09:38 +02:00
Shadowghost c77649d21e Skip alternate version links when resolving link parents 2026-08-14 07:39:49 +02:00
Vitalijus 8f97e690c8 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-14 04:01:38 +00:00
Marc Brooks b557dcfa45 Fix other two missing DE localization DE updates
Turns out there were two more instances of test broken by commit 21fec95b07
2026-08-13 22:31:42 -05:00
st7105 7b66b32cdf Recognize WEB-DL release tags in video names 2026-08-14 06:03:42 +03:00
Cody Robibero 7f3bbd5654 Merge pull request #17633 from IDisposable/chore/fix-de-localization-unit-test
Fix unit test for localization of Artists in DE
2026-08-13 18:37:14 -04:00
Marc Brooks a7c0c92019 Fix unit test for localization
DE localization for Artists was changed in localization commit 21fec95b07
2026-08-13 17:11:31 -05:00
Cody Robibero e1a619b79e Merge pull request #17615 from jellyfin/renovate/microsoft
Update Microsoft to 10.0.11
2026-08-13 16:27:06 -04:00
Cody Robibero 12226d2212 Merge pull request #17604 from theguymadmax/fix-FindArtists
Fix FindArtists
2026-08-13 16:26:48 -04:00
ZeroAiden ae8723026d Translated using Weblate (Malay)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ms/
2026-08-13 04:04:40 +00:00
Vitalijus a730d8a7ba Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-12 23:27:15 +00:00
Psychotrickser 21fec95b07 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/
2026-08-12 12:00:21 +00:00
Shadowghost 7e6709f023 Fix formatting 2026-08-12 08:48:42 +02:00
Shadowghost a4ff630d1f Fix English metadata blocking localized providers ranked below it 2026-08-12 08:39:00 +02:00
Shadowghost 9a07d4336b Make the media stream filter index covering 2026-08-12 08:16:09 +02:00
Shadowghost 2b5625d1c4 Resolve link owners from LinkedChildren 2026-08-12 08:16:03 +02:00
Shadowghost 9ae6ffe441 Share the SQLite fixture across the item tests 2026-08-12 08:15:55 +02:00
renovate[bot] 36fccdf6d4 Update Microsoft to 10.0.11 2026-08-12 04:43:32 +00:00
Bond-009 4e2fc33a61 Merge pull request #17551 from jellyfin/renovate/z440.atl.core-7.x
Update dependency z440.atl.core to 7.16.0
2026-08-11 18:17:55 +02:00
Shadowghost fa7fdf5884 Optimize query helper memory 2026-08-11 18:09:23 +02:00
renovate[bot] 2deb97bac3 Update dependency z440.atl.core to 7.16.0 2026-08-11 14:01:27 +00:00
theguymadmax 9220c2bd63 Fix FindArtists 2026-08-11 09:40:11 -04:00
benvin 2e1e445e47 Merge pull request 'ci: add Woodpecker build+test pipeline, drop GitHub Actions' (#2) from benvin/woodpecker-ci into main
ci/woodpecker/push/ci Pipeline was successful
Reviewed-on: #2
2026-08-11 21:14:33 +10:00
benvin 4920aa871a Merge pull request 'Gate periodic library-mutating tasks behind a scan-leader lease' (#1) from benvin/scan-leader-election into main
HA Build & Push to ECR / build-and-push (push) Has been cancelled
Reviewed-on: #1
2026-08-11 21:11:58 +10:00
Cody Robibero 6d501ba418 Merge pull request #17570 from Shadowghost/fix-ef-drift
Fix EF core designer drifts
2026-08-10 19:13:29 -04:00
Cody Robibero 2a8d168796 Merge pull request #17579 from Shadowghost/fix-removal-notification
Fix missing ItemRemoved events and search fallback after access filtering
2026-08-10 18:28:22 -04:00
Cody Robibero 34cc90f5e2 Merge pull request #17599 from Shadowghost/unbreak-master
Fix master build
2026-08-10 17:29:12 -04:00
unkinben a06b11980e ci: add Woodpecker build+test pipeline, drop GitHub Actions
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Why:
- The fork inherited GitHub Actions workflows that target the upstream's
  self-hosted GitHub runners and do not run on this Gitea/Woodpecker
  infrastructure, so source changes (such as the scan-leader lease work)
  currently land without any CI validation.

How:
- Add .woodpecker/ci.yaml running restore, build and test of Jellyfin.sln
  on pull_request and push, using the .NET 9 SDK that global.json pins.
- Filter out RequiresDocker and Integration tests, mirroring the upstream
  test selection so the suite runs without extra services.
- Set memory-heavy resource requests/limits and a dedicated
  serviceAccountName for the build+test step.
- Remove the inherited .github/workflows/ pipelines that only run on the
  upstream's GitHub Actions runners.
2026-08-11 07:23:43 +10:00
Shadowghost 4e9713a032 Apply review suggestions 2026-08-10 23:10:33 +02:00
Shadowghost f82101332d Fix master build 2026-08-10 23:04:01 +02:00
Shadowghost 7b6ae06f3b Merge remote-tracking branch 'upstream/master' into safeguard-invalid-provider-ids 2026-08-10 23:03:52 +02:00
Shadowghost 26fbd1eac0 Fix master build 2026-08-10 22:46:31 +02:00
Cody Robibero 35e86416af Merge pull request #17576 from obiwantoby/perf/batch-mediasourcecount-dto
Bugfix: #17547 | Batching MediaSourceCount into one call
2026-08-10 16:15:54 -04:00
Cody Robibero c210ea87f2 Merge pull request #17597 from theguymadmax/fix-people-filtering
Fix PersonTypes not applied when filtering by person
2026-08-10 16:14:19 -04:00
Cody Robibero 4a1fcca182 Merge pull request #17582 from Shadowghost/fix-assemblies
Fix assemblies
2026-08-10 16:12:27 -04:00
Cody Robibero f5211a6722 Merge pull request #17590 from TOomaAh/fix/limit-remote-fetch-similar
fix: bound remote provider pagination
2026-08-10 16:12:11 -04:00
Cody Robibero 0b77a520fa Merge pull request #17588 from TOomaAh/fix/is-airing-filter
fix: correct IsAiring negation to exclude airing items
2026-08-10 16:11:30 -04:00
Cody Robibero d0e0e291f2 Merge pull request #17569 from vavallee/fix/17056-no-image-upscaling
Stop image endpoints from upscaling beyond the source resolution
2026-08-10 16:10:17 -04:00
Cody Robibero 82ecfde2f7 Update MediaBrowser.Controller/Drawing/ImageHelper.cs
Co-authored-by: Tim Eisele <Tim_Eisele@web.de>
2026-08-10 16:10:08 -04:00
Cody Robibero 7be16e9f74 Merge pull request #17563 from Shadowghost/cleanup-sql-helpers
Cleanup and simplify query helpers
2026-08-10 16:10:00 -04:00
theguymadmax f441c5f125 Fix PersonTypes not applied when filtering by person 2026-08-10 11:03:14 -04:00
unkinben 0008bde28e Gate periodic library-mutating tasks behind a scan-leader lease
ABI Compatibility / ABI - HEAD (pull_request) Has been cancelled
ABI Compatibility / ABI - BASE (pull_request) Has been cancelled
OpenAPI / OpenAPI - HEAD (pull_request) Has been cancelled
OpenAPI / OpenAPI - BASE (pull_request) Has been cancelled
Tests / run-phase5-tests (pull_request) Has been cancelled
Tests / run-tests (pull_request) Has been cancelled
Project Automation / Project board (pull_request) Has been cancelled
Merge Conflict Labeler / Labeling (pull_request) Has been cancelled
ABI Compatibility / ABI - Difference (pull_request) Has been cancelled
OpenAPI / OpenAPI - Difference (pull_request) Has been cancelled
OpenAPI / OpenAPI - Publish Unstable Spec (pull_request) Has been cancelled
OpenAPI / OpenAPI - Publish Stable Spec (pull_request) Has been cancelled
In a multi-pod deployment every pod runs the scheduled-task timers, so
periodic library-mutating tasks (library refresh, people/chapter refresh,
audio normalization, media-segment and keyframe extraction, collection and
user-data cleanup, database optimization) fire concurrently against the shared
database and library, duplicating work and racing each other.

Add an IScanLeaderLease abstraction that elects a single scan leader via a
Redis TTL lease keyed on the pod identity, mirroring the existing transcode
lease machinery. RedisScanLeaderLease acquires or renews the lease with an
atomic Lua script and fails safe by treating the pod as leader whenever Redis
is unreachable, so scans never stall. NullScanLeaderLease preserves the
single-instance behavior when election is disabled or no Redis connection is
configured.

Gate only the timer-driven path in ScheduledTaskWorker: when election is
enabled and a task key is in the gated set, a non-leader re-arms its trigger
and skips enqueueing. Manual and API-triggered runs bypass this path and still
run on any pod. Wiring is additive and the new worker constructor parameters
are optional, so existing behavior is unchanged when election is off.

Signed-off-by: Ben Vincent <ben@unkin.net>
2026-08-10 23:51:18 +10:00
hoanghuy309 e6e3099da9 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/vi/
2026-08-10 10:46:24 +00:00
Kiki d7dd98858e Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2026-08-10 10:46:24 +00:00
Dan Tsivinsky 3d3d3c26b8 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/
2026-08-10 10:46:23 +00:00
TOomaAh 5f3e938d09 fix: bound remote provider pagination 2026-08-09 17:18:54 +02:00
TOomaAh 3031aa91ba fix: correct IsAiring negation to exclude airing items 2026-08-09 16:28:20 +02:00
Vitalijus fb763c47bf Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-08 21:55:48 +00:00
Doougfoks a65b614449 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/
2026-08-08 20:28:52 +00:00
brandon 505a6ebf8f Use WhereOneOrMany helper for the alternate version id filter
Filter the parent ids with the WhereOneOrMany query helper instead of a
raw Contains so the id list is wrapped in EF.Parameter and EF Core reuses
one compiled query plan across calls, matching how the rest of the item
queries build their id filters.
2026-08-08 15:20:39 -04:00
Shadowghost 8c4dfc0b71 Safeguard against invalid provider ids 2026-08-08 19:49:26 +02:00
brandon 10d108a1f4 Address review on MediaSourceCount batching
Rename GetItemsWithAlternateVersions to GetItemIdsWithAlternateVersions
across the interfaces and implementations since it returns ids. Return
the hashset straight from the query instead of materializing an array
first. Rename the DtoService guard to mayHaveAlternateVersions and
invert it so the computed path is the explicit case. Assert the media
source count value in the batch skip test and add a test covering an
item that is in the returned set still resolving to the correct count.
2026-08-08 12:33:11 -04:00
Shadowghost 428c0f19bc Fix bump_version script 2026-08-08 18:26:46 +02:00
Shadowghost afd91a3d9d Fix assemblies 2026-08-08 18:20:19 +02:00
queeup a0b291a463 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2026-08-08 16:14:51 +00:00
Thadah D. Denyse a9e4c826aa Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eu/
2026-08-08 14:03:01 +00:00
hoanghuy309 b189813eb1 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/vi/
2026-08-08 14:02:57 +00:00
queeup 7c20ea2708 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2026-08-08 14:02:53 +00:00
Shadowghost e63c05137a Fix missing ItemRemoved events and search fallback after access filtering 2026-08-08 10:59:37 +02:00
brandon c091ffdc6b Batch alternate version detection in DtoService to remove MediaSourceCount N+1
Browsing a page of videos with the MediaSourceCount field ran one alternate
version query per item, each opening a fresh DbContext. On a large library that
turned a single page into hundreds of sequential round trips and made the Items
endpoint take tens of seconds while holding a request thread the whole time.

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

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

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

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

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

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

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

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

Measured against master with a 62-track music library:

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

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

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

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

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

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

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

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

To fix this:

we lock transcoding job state changes and segment handling per playlist, and use a thread safe counter to track how many http responses are still using each job’s segments. A job is only stopped or replaced once that counter reaches zero.
2026-08-05 00:33:42 +08:00
Shed Shedson 7fbc1ff8c0 Translated using Weblate (Icelandic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/is/
2026-08-04 11:29:56 +00:00
Yunseo Jung 5767a3a800 Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ko/
2026-08-04 11:29:55 +00:00
Vincenzo Reale 4b51ef2c2f Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-08-04 11:29:55 +00:00
krvi f50fc0fa86 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-03 23:01:06 +00:00
potuzhnyj a817391f67 Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/
2026-08-03 23:01:06 +00:00
Erik W cc877229b1 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sv/
2026-08-03 23:01:05 +00:00
Vitalijus a3e7024355 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lt/
2026-08-03 23:01:04 +00:00
Shadowghost d3c7aa6bd2 Fix /Persons favourites timing out by removing the correlated favourites join 2026-08-04 00:40:53 +02:00
Shadowghost 40601523e7 Degrade ForceKeepAlive logs to debug 2026-08-03 21:30:22 +02:00
krvi e3664a9561 Translated using Weblate (Faroese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/
2026-08-03 18:59:02 +00:00
nextlooper42 a1ed4b9001 Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/
2026-08-03 18:59:02 +00:00
Vincenzo Reale e09492de62 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/
2026-08-03 18:59:01 +00:00
myrad2267 d10381a0e2 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/
2026-08-03 18:59:01 +00:00
myrad2267 9596115c45 Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr_CA/
2026-08-03 18:59:00 +00:00
Translation expert ed0be243e9 Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/
2026-08-03 18:58:59 +00:00
Shadowghost f502fd312e Fix test 2026-08-03 15:29:32 +02:00
rlauuzo 9d0dffe818 Clear metadata provider cache when provider parts are registered
AddParts replaces _metadataProviders but left _metadataProviderCache
holding provider arrays from the previous registration. Invalidate the
cache so repeated AddParts calls cannot serve stale providers, and give
the previously uncalled ClearMetadataProviderCache its intended caller.
2026-08-03 11:56:08 +02:00
Shadowghost 4e2089b6a1 Keep folder extras with the item that owns the folder 2026-08-03 10:50:33 +02:00
Shadowghost 55518c06ee Only treat series and seasons as resumable folders 2026-08-03 09:01:55 +02:00
Shadowghost a272efb9a2 Fix disabled plugins being re-enabled on restart 2026-08-03 08:48:09 +02:00
Robin 33a8cdfc0b Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_419/
2026-08-03 06:20:33 +00:00
Robin 38c6c5b478 Translated using Weblate (Spanish (Mexico))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_MX/
2026-08-03 06:20:32 +00:00
Robin d759df03b5 Translated using Weblate (Spanish (Argentina))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/
2026-08-03 06:20:31 +00:00
Kaz e2c1121079 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/
2026-08-03 06:20:29 +00:00
無情天 fe3585577b Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/
2026-08-03 01:53:49 +00:00
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
mat d4f9c12c22 fix: downgrade PostgreSQL provider to net9.0/EF Core 9.x for v10.11.7 compat
HA Build & Push to ECR / build-and-push (push) Has been cancelled
Upstream Jellyfin 10.11.7 targets net9.0. Our PostgreSQL provider was on
net10.0 with Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0 which requires
EF Core 10+. Downgrade to 9.0.4 to match upstream's EF Core 9.0.11.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 22:57:39 -04:00
ZoltyMat d46539b718 docs: add architecture, contributing guide, and trim Dockerfile comment (#4)
* docs: add architecture overview, contributing guide, and GitHub discussion draft

ARCHITECTURE.md covers server layer diagram, subsystems, and runtime info.
CONTRIBUTING.md covers dev setup, build, test, and submission workflow.
GITHUB-DISCUSSION-DRAFT.md drafts the upstream discussion post for the HA fork.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: trim verbose comment in Dockerfile.runtime

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 22:56:34 -04:00
mat 13f49305da security: remove pull_request trigger from ha-build.yml
ha-build.yml runs on self-hosted k3s runners. Having pull_request as a
trigger allows any internet user to open a PR against this public repo
and execute arbitrary code on cluster nodes (GitHub does block secret
injection on fork PRs, but runner filesystem and cluster network access
remain).

Removed pull_request trigger. Build-on-push-to-main is sufficient.
CI test feedback on PRs is covered by ci-tests.yml which uses
GitHub-hosted (ubuntu-latest) runners only.
2026-03-31 22:56:34 -04:00
ZoltyMat 7d4cef51f5 feat: add Helm chart for jellyfin-ha (#3)
Adds a production-ready Helm chart under deploy/helm/jellyfin-ha/.

Motivated by a community request on Reddit:
https://www.reddit.com/r/JellyfinCommunity/comments/1rvj17f/jellyfin_ha_on_kubernetes_redisbacked_transcode/oav7mlz/

Features:
- StatefulSet with configurable replica count (default 2 for HA)
- Redis subchart (in-cluster) wired to ITranscodeSessionStore via
  Jellyfin__TranscodeStore__RedisConnectionString env var
- Supports external Redis via ha.transcodeStore.existingSecret or
  ha.transcodeStore.redisConnectionString
- Optional in-cluster PostgreSQL StatefulSet (experimental, mirrors
  existing kubernetes/apps/media/jellyfin-postgres.yaml pattern)
- RWX config + transcode PVCs (required for multi-pod session takeover)
- Per-pod cache via volumeClaimTemplates (RWO)
- Optional NFS PV+PVC for media library
- Intel QSV / VA-API GPUgit checkout -b feat/helm-chart && git add deploy/ && legit add deploy/ && git commit -m dagit commit -m featss
2026-03-31 22:56:34 -04:00
mat a9aa2d53ed docs: add FORK-DIFF.md summarising all changes vs upstream jellyfin/jellyfin 2026-03-31 22:56:34 -04:00
mat 08b87504b4 docs: add Docker Compose, k8s manifests, bare dotnet HA setup, and DigitalOcean link 2026-03-31 22:56:34 -04:00
mat df27faaa41 docs: rewrite README with HA setup guide, config reference, and architecture 2026-03-31 22:56:34 -04:00
Copilot fe9ea18918 Phase 5.2.4: Tune HLS segmentation for HA recovery (#29)
* Initial plan

* Phase 5.2.4: Tune HLS behavior for recovery with configurable segment parameters

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

* fix: SA1516 — add blank line between MakeLiveStreamKey and Clone helpers

StyleCop SA1516 requires elements to be separated by blank lines.
Missing blank line at line 370 caused build failure in Phase 5 tests.

Closes #28

* fix: SA1516 — blank line in InMemoryTranscodeSessionStore between helpers

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
Co-authored-by: mat <mstrommen@gmail.com>
2026-03-31 22:56:30 -04:00
Copilot e71efc3f97 Fix SessionManager._activeLiveStreamSessions for HA pod takeover safety (#27)
* Initial plan

* Issue 5.2.3b: Fix SessionManager._activeLiveStreamSessions for takeover safety

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

* ci: trigger CI run for PR #27 review

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
Co-authored-by: mat <mstrommen@gmail.com>
2026-03-31 22:56:30 -04:00
Copilot 9817185fa3 Add lease-aware cleanup to DeleteTranscodeFileTask (#25)
* Initial plan

* Add GetActiveSessionsAsync to ITranscodeSessionStore and update DeleteTranscodeFileTask for lease-aware cleanup

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

* Fix Redis exception propagation in GetActiveSessionsAsync for safe abort behavior

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

* fix: use KeysAsync to resolve CA1849 analyzer violation

Replace synchronous IServer.Keys() with async IServer.KeysAsync()
using await foreach to satisfy CA1849 (TreatWarningsAsErrors).

CA1849: 'IServer.Keys()' synchronously blocks.
Await 'IServer.KeysAsync()' instead.

Line 161 in RedisTranscodeSessionStore.GetActiveSessionsAsync.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
Co-authored-by: mat <mstrommen@gmail.com>
2026-03-31 22:56:30 -04:00
Copilot c2a11f3e68 Phase 5.2.2a: Register HLS sessions in ITranscodeSessionStore + lease renewal (#23)
* Initial plan

* Phase 5.2.2a: Register HLS sessions in ITranscodeSessionStore + lease renewal

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:56:30 -04:00
Copilot d60ae43b59 Wire ITranscodeSessionStore to Redis-backed impl with NullTranscodeSessionStore fallback and DI registration (#21)
* Initial plan

* feat: add Redis-backed ITranscodeSessionStore with NullTranscodeSessionStore fallback and DI registration

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

* refactor: add code review improvements - lease expiry comment, Redis connection error handling

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:56:30 -04:00
Copilot a187ab18b8 Add ITranscodeSessionStore interface and HA recovery unit tests (#19)
* Initial plan

* Add ITranscodeSessionStore interface, TranscodeSession record, InMemoryTranscodeSessionStore fake, and HA unit tests"

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:56:30 -04:00
ZoltyMat cf5268c1e4 docs: Phase 5.1.1 - HA-TRANSCODING-DESIGN.md transcode lifecycle audit (#17)
Map the exact transcode lifecycle before Phase 5.2 code changes begin.
No functional code changes.

Key findings documented:
- _activeTranscodingJobs: process-local, not persisted across pod restarts
- playSessionId: caller-supplied nullable string; three HA failure modes
- DeleteTranscodeFileTask: age-only cleanup, unsafe for shared NFS storage
- _activeLiveStreamSessions: process-local ConcurrentDictionary, cannot be
  inherited by a takeover pod without durable store rehydration
- NFSv3 confirmed (nfsvers=3). Close-to-open consistency advisory; recovery
  must skip the last incomplete segment and restart one segment earlier
- Minimum recovery state: 10 fields including server-generated sessionId,
  ownerPod, manifestPath, lastCompletedSegmentIndex, lastHeartbeatUtc

Closes Phase 5 Issue 5.1.1
2026-03-31 22:56:30 -04:00
ZoltyMat f405e17c96 ci: Phase 5.0.4b - add run-phase5-tests gate to ci-tests.yml (#16)
Add a dedicated 'run-phase5-tests' job that runs the three test assemblies
most affected by Phase 5 HLS session-sharing and PostgreSQL media-encoding
changes in parallel with the existing full-matrix run-tests job.

Targeted assemblies:
- tests/Jellyfin.Api.Tests (HLS controller surface)
- tests/Jellyfin.MediaEncoding.Hls.Tests (transcode lifecycle)
- tests/Jellyfin.Server.Implementations.Tests (RedisTranscodeSessionStore)

Each test step filters Category!=RequiresDocker to exclude Docker-dependent
tests that require Testcontainers. Coverage results are merged separately
into merged-phase5/ to avoid collisions with the full-matrix merged/ dir.

Closes #(Phase 5.0.4b)
2026-03-31 22:56:30 -04:00
ZoltyMat 67300ec17f ci: add PR trigger, copilot/* and feat/phase* branches, concurrency block (issue 5.0.0a) (#15)
- Add pull_request: trigger so Copilot agent branches get CI coverage
- Add feat/phase* and copilot/* to push branch list
- Add concurrency block to cancel stale runs on same ref
- Set push: false on PR events (build only, no ECR push for PRs)
2026-03-31 22:56:30 -04:00
mat 2b4a0a2314 fix(docker): bundle jellyfin-web into Dockerfile.runtime (the CI-used file)
Previous fixes went to Dockerfile, but CI uses Dockerfile.runtime.
Add webclient stage: installs jellyfin-web=10.11.6+deb12 from the
Jellyfin bookworm apt repo (correct version suffix: +deb12, not +1).
Web assets land at /usr/share/jellyfin/web/ and are copied to
/jellyfin/jellyfin-web/ in the runtime image.
Add --webdir flag to ENTRYPOINT explicitly.
2026-03-31 22:56:30 -04:00
mat d84a37e1d2 fix(docker): use jellyfin-web 10.11.6+deb12 from jellyfin apt repo
Previous attempt used 10.9.11+1 which does not exist — the bookworm
repo uses +deb12 suffix (e.g. 10.11.6+deb12) and only has >= 10.11.x.
This caused an empty /jellyfin/jellyfin-web/ at runtime, making the
server crash with 'content directory is either invalid or empty'.

Fix: install jellyfin-web=10.11.6+deb12 from the official Jellyfin
bookworm apt repo. Assets land at /usr/share/jellyfin/web/ and are
COPY'd to /jellyfin/jellyfin-web/ in the runtime image.
2026-03-31 22:56:30 -04:00
mat 465b6c5d9e fix(docker): install jellyfin-web 10.9.11 via apt repo into /jellyfin/jellyfin-web
Pulling from jellyfin/jellyfin:10.9.11 multi-stage yielded an empty
/jellyfin/jellyfin-web directory (the official image likely has a different
internal structure). Switch to the official Jellyfin apt repo instead:
  apt-get install jellyfin-web=10.9.11+1
  web assets land at /usr/share/jellyfin/web/
  COPY to /jellyfin/jellyfin-web/ in runtime image

--webdir /jellyfin/jellyfin-web is already in ENTRYPOINT.
2026-03-31 22:56:30 -04:00
mat b975195252 fix(docker): bundle jellyfin-web 10.9.11 from official image into wwwroot
wwwroot/ only contained api-docs (Swagger) — the web client was never bundled.
Add a webclient stage that pulls /jellyfin/jellyfin-web from jellyfin/jellyfin:10.9.11
and copies it to /jellyfin/jellyfin-web in the runtime image.

Pass --webdir /jellyfin/jellyfin-web to the entrypoint so the server serves the UI.
jellyfin-web 10.9.11 is API-compatible with the 10.12.0 server fork.
Update to a matching 10.12.x web client once that release ships upstream.
2026-03-31 22:56:30 -04:00
mat 1c28df61df fix(ha): PostgreSQL migration backup no-op + URI connection string support
- MigrationBackupFast/RestoreBackupFast/DeleteBackup return no-ops for
  PostgreSQL; pre-migration backups are handled by jellyfin-pg-backup
  CronJob, not the automated backup path that throws NotSupportedException

- ServiceCollectionExtensions: detect postgresql:// / postgres:// URI
  format in POSTGRES_CONNECTION_STRING and convert to ADO.NET key=value
  format before passing to NpgsqlDataSourceBuilder (which requires it)

Closes startup crash: 'Automated migration backups are not supported for
PostgreSQL' on first boot with a fresh database.
2026-03-31 22:56:30 -04:00
mat c69b8ccc53 fix: add missing using for IServerConfigurationManager 2026-03-31 22:56:29 -04:00
mat 138081bd8b fix(ha): register IServerConfigurationManager in pre-startup DI for PostgreSQL
The NpgsqlDataSource singleton factory in AddJellyfinDbContext calls
sp.GetRequiredService<IServerConfigurationManager>() to read pool settings.
During ApplyStartupMigrationAsync, only a subset of services are registered
in the startup service collection — IServerConfigurationManager was missing,
causing a fatal DI resolution failure when DatabaseType=Jellyfin-PostgreSQL.

Fix: register startupConfigurationManager as IServerConfigurationManager in
the migrationStartupServiceProvider service collection.

Also remove JELLYFIN_CONFIG_DIR=/config from Dockerfile.runtime ENV block.
When configDir == dataDir, MakeSanityCheckOrThrow writes .jellyfin-config at
the datadir root, then immediately throws because it expected .jellyfin-data.
Removing the env var lets configDir default to $JELLYFIN_DATA_DIR/config.
2026-03-31 22:56:29 -04:00
mat ea732b62d8 fix(ci): pre-build .NET on host runner, remove SDK from Docker build
DinD overlay-on-overlay throttles dotnet child container to ~11s CPU/3h wall time.
Solution: dotnet restore+publish run on native runner FS (~10min), then
Dockerfile.runtime just COPYs the pre-built publish-output/ directory.
This brings build time from 3h+ (stuck) to ~10-15 minutes total.
2026-03-31 22:56:29 -04:00
mat e7c1451b6c fix(ci): exclude integration tests — need SkiaSharp native libs and running server 2026-03-31 22:56:26 -04:00
mat a49a766424 fix(ci): export PATH in test step — each step has fresh shell on ARC runners 2026-03-31 22:56:26 -04:00
mat ddf3b9f609 fix(ci): use GITHUB_ENV not GITHUB_PATH for dotnet PATH on ARC runners
GITHUB_PATH between-step propagation is broken on summerwind ARC runner pods.
Switch to GITHUB_ENV PATH= which is reliably sourced on every step.
2026-03-31 22:56:26 -04:00
mat 9c6fab8f12 fix(ci): install .NET 10 via script to writable DOTNET_INSTALL_DIR
setup-dotnet fails on ARC runners (no permission to /usr/share/dotnet) and dotnet
is not pre-installed. Install via dotnet-install.sh into $HOME/.dotnet instead,
then add to PATH via GITHUB_PATH.
2026-03-31 22:56:26 -04:00
mat 992e591938 fix(ci): remove setup-dotnet from test workflow — ARC runners have .NET 10 pre-installed
setup-dotnet fails on self-hosted ARC runners because it cannot write to
/usr/share/dotnet (permission denied). The runner images already have .NET 10
SDK installed in DOTNET_ROOT. Remove the step entirely.
2026-03-31 22:56:26 -04:00
mat 365036f9f2 fix(ci): skip Docker-dependent PostgreSQL tests in upstream test workflow
- Add [Trait("Category", "RequiresDocker")] to all 3 PostgreSQL test classes
  (PostgreSqlMigrationTests, PostgreSqlProviderTests, PostgreSqlConcurrencyTests)
- Add --filter "Category!=RequiresDocker" to ci-tests.yml dotnet test command
  so runners without Docker don't fail on Testcontainers initialization
- Disable CodeQL workflow in fork (requires upstream org permissions + .NET 10
  CodeQL support that isn't available on our self-hosted runners)

PostgreSQL tests still run in ha-build.yml against the cluster where Docker
is available via the self-hosted ARC runners.
2026-03-31 22:56:13 -04:00
mat 93af601a5f ci: run tests and CodeQL on k3s self-hosted amd64 runners
- ci-tests.yml: drop ubuntu/macos/windows matrix → single k3s amd64 runner
- ci-codeql-analysis.yml: ubuntu-latest → k3s amd64 runner
Fork targets amd64 cluster only; self-hosted runners have Docker for Testcontainers
2026-03-31 22:56:13 -04:00
mat 894d837578 fix(ci): use version tags for docker actions 2026-03-31 22:56:13 -04:00
mat e79caccab1 fix(ci): add docker/setup-buildx-action before build step 2026-03-31 22:56:13 -04:00
mat 25b137e0ab fix(ci): switch ha-build to static AWS key auth 2026-03-31 22:56:13 -04:00
mat 75ef410437 fix(ci): trigger ha-build workflow on master branch 2026-03-31 22:56:13 -04:00
mat 849f0a06c8 fix(dockerfile): disable TreatWarningsAsErrors for Docker publish step
StyleCop analyzer violations in upstream src/Jellyfin.Extensions/ block
the Docker build when TreatWarningsAsErrors=true (from Directory.Build.props).
Disable for container image builds — StyleCop enforcement is the CI pipeline's
responsibility, not the Dockerfile's.
2026-03-31 22:56:10 -04:00
mat 2e5fb57052 fix(dockerfile): copy BannedSymbols.txt and stylecop.json into build stage
dotnet publish failed with CS2001 because these root-level analyzer config files
were not included in the Docker build context. They are referenced by
Directory.Build.props and required by StyleCop and BannedApiAnalyzers at build
time.
2026-03-31 22:56:10 -04:00
Copilot e72bde9c02 Add Jellyfin.DbMigrator SQLite-to-PostgreSQL migration tool (#14)
* Initial plan

* Add Jellyfin.DbMigrator SQLite-to-PostgreSQL migration tool

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:56:07 -04:00
Copilot 35a1ec8a5d [WIP] Add PostgreSQL integration test project for validation (#12)
* Initial plan

* Add PostgreSQL integration test project with migration, CRUD, and concurrency tests

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:55:57 -04:00
Copilot a0c38131c8 Wire NpgsqlDataSource pool into DI for PostgreSQL provider (#10)
* Initial plan

* Add NpgsqlDataSource pool wiring to DI (Issue 1.4)"

- PostgreSqlDatabaseProvider: accept NpgsqlDataSource via constructor injection, use it in Initialise()
- PostgreSqlDesignTimeJellyfinDbFactory: build NpgsqlDataSource from connection string for design-time use
- ServiceCollectionExtensions: register NpgsqlDataSource as singleton with pool params (MinPoolSize=2, MaxPoolSize=20, CommandTimeout=30) from CustomProviderOptions.Options

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:55:45 -04:00
Copilot 3bc45ff92d Add PostgreSQL EF Core design-time factory and InitialPostgreSql migration (#8)
* Initial plan

* Add PostgreSQL design-time factory and initial migration for all 29 DbSets

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

* Remove accidentally committed build artifacts from PostgreSQL provider

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:55:45 -04:00
Copilot 2d4d6d8c38 Implement PostgreSqlDatabaseProvider methods and DI registration (#6)
* Initial plan

* feat: implement PostgreSqlDatabaseProvider all methods + DI registration

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:55:45 -04:00
Copilot 9a99572354 Scaffold Jellyfin.Database.Providers.PostgreSQL project (#4)
* Initial plan

* Issue 1.1: Scaffold PostgreSQL provider project

Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com>
2026-03-31 22:55:45 -04:00
Joshua M. Boniface b2aa80ce5c Fix invalid regex comparison 2026-03-31 19:59:33 -04:00
Joshua M. Boniface ff365dae34 Fix invalid merge conflict fix 2026-03-31 19:46:47 -04:00
Jellyfin Release Bot 52aebfb7d3 Bump version to 10.11.7 2026-03-31 19:33:11 -04:00
Joshua M. Boniface 66ea1b50e6 Merge commit from fork
Fix GHSA-8fw7-f233-ffr8 with improved sanitization
2026-03-31 19:17:17 -04:00
Joshua M. Boniface 3f656ade7a Merge remote-tracking branch 'upstream/release-10.11.z' into advisory-fix-1 2026-03-31 19:16:19 -04:00
Joshua M. Boniface 8bf0d372c6 Merge commit from fork
Fix GHSA-jh22-fw8w-2v9x
2026-03-31 17:46:01 -04:00
Joshua M. Boniface 202d7b5829 Merge branch 'release-10.11.z' into advisory-fix-1 2026-03-31 17:44:59 -04:00
Joshua M. Boniface 352e4f3aba Merge commit from fork
Fix GHSA v2jv-54xj-h76w
2026-03-31 17:43:02 -04:00
Joshua M. Boniface c5f6d00c94 Merge commit from fork
Fix GHSA-j2hf-x4q5-47j3 with improved sanitization
2026-03-31 17:38:46 -04:00
Shadowghost e8d1d94436 Lock down tuner API to be admin-only 2026-03-31 16:35:15 +02:00
Shadowghost 50dc37065b Fix GHSA-jh22-fw8w-2v9x 2026-03-31 09:30:45 +02:00
Niels van Velzen 7e88b18192 Merge pull request #16522 from Bond-009/CA1810
Fix CA1810 build error
2026-03-30 18:44:15 +02:00
Bond-009 89e914c7f1 Merge pull request #16519 from jellyfin/check-h264-profile-null
Fix Null was not checked before using the H264 profile
2026-03-30 18:39:45 +02:00
Bond_009 1932ac4765 Fix CA1810 build error 2026-03-30 18:33:56 +02:00
Bond-009 ec33c74ec4 Merge pull request #16440 from Molier/fix/subtitle-extraction-flush
Remove -copyts and add -flush_packets 1 to subtitle extraction
2026-03-30 18:30:58 +02:00
nyanmisaka 2184ed1b16 Fix Null was not checked before using the H264 profile
This is rare, but not impossible.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-03-30 20:51:11 +08:00
Shadowghost d3907afde7 Add additional validations 2026-03-30 10:48:51 +02:00
theguymadmax e12d933531 Revet lint fix 2026-03-30 04:21:26 -04:00
theguymadmax c0ba29d917 fix lint issue 2026-03-30 04:14:23 -04:00
Shadowghost d1fd81c382 Fix GHSA v2jv-54xj-h76w 2026-03-30 09:40:01 +02:00
Joshua M. Boniface e038045494 Fix lint 2026-03-29 19:11:40 -04:00
Joshua M. Boniface e1691e649e Merge pull request #16514 from theguymadmax/release-10.11.z-fixup 2026-03-29 19:04:53 -04:00
theguymadmax 8d28497d29 Fix lint issue 2026-03-29 18:25:57 -04:00
Joshua M. Boniface fddd4e7e6b Fix GHSA-8fw7-f233-ffr8 with improved sanitization
Co-Authored-By: Shadowghost <Ghost_of_Stone@web.de>
2026-03-29 17:30:09 -04:00
Joshua M. Boniface 0581cd6610 Fix GHSA-j2hf-x4q5-47j3 with improved sanitization
Co-Authored-By: Shadowghost <Ghost_of_Stone@web.de>
2026-03-29 17:22:14 -04:00
Joshua M. Boniface 0f1732e5f5 Merge pull request #16425 from theguymadmax/fix-metadata-backup 2026-03-29 15:22:14 -04:00
Bond-009 41c2d51d8c Merge pull request #16369 from Bond-009/skiafonts
Fix nullref ex in font handling
2026-03-29 20:27:17 +02:00
Bond-009 29b2361857 Merge pull request #16423 from nyanmisaka/fix-ffmpeg8-readrate-option
Fix readrate options in FFmpeg 8.1
2026-03-23 21:25:35 +01:00
Bond-009 ce867f9834 Merge pull request #16449 from theguymadmax/fix-collection-number
Fix NFO saver using wrong provider ID for collectionnumber
2026-03-23 19:23:34 +01:00
theguymadmax 4034bf9d7e Save collection id instead of moive id 2026-03-22 12:49:33 -04:00
Oscar 3d2658fa43 Remove -copyts and add -flush_packets 1 to subtitle extraction
-copyts is unnecessary for -c:s copy to SRT and slows extraction ~5x.
Without -flush_packets 1, ffmpeg buffers all output until exit, leaving
.srt files at 0 bytes for minutes while the player shows no subtitles.

Fixes #16438
2026-03-19 22:54:05 +01:00
theguymadmax 61b19688ff Backup default metadata location 2026-03-17 23:13:20 -04:00
theguymadmax e8d72bf6a3 Fix restore backup metadata location 2026-03-16 10:32:09 -04:00
nyanmisaka 348b14f7b7 Fix readrate options in FFmpeg 8.1
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-03-16 17:58:53 +08:00
IceStormNG fda49a5a49 Apply analyzeduration and probesize for subtitle streams to improve codec parameter detection (#16293)
Apply analyzeduration and probesize for subtitle streams to improve codec parameter detection
2026-03-13 20:26:25 +01:00
Bond-009 55c00d76bb Merge pull request #16392 from nyanmisaka/fix-ffmpeg8-filter-detection
Fix filter detection in FFmpeg 8.1
2026-03-13 20:24:18 +01:00
nyanmisaka 519d2113eb Fix filter detection in FFmpeg 8.1
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2026-03-11 17:46:07 +08:00
Bond_009 f34f6b6941 Fix nullref ex in font handling
Don't add fallback fonts if they are null
The default font can still be null, however unlikely
2026-03-08 12:46:25 +01:00
Joshua M. Boniface 6864e108b8 Merge pull request #16257 from lowbit/fix/subtitle-empty-file-cache 2026-03-07 00:53:56 -05:00
Bond-009 09ba04662a Merge pull request #16341 from crimsonspecter/fix-fractional-hls-time-for-remux
Fix hls segment length adjustment for remuxed content
2026-03-06 22:39:59 +01:00
crimsonspecter 9cd2418095 Fix: don't apply segment length adjustment for remuxed content 2026-03-04 20:27:06 +01:00
Bond-009 b6a96513de Merge pull request #16253 from theguymadmax/use-backupdatabase
Checkpoint WAL before moving library.db in migration
2026-02-28 10:08:36 +01:00
Bond-009 ca57166e95 Merge pull request #16204 from MBR-0001/fix-bad-subtitle-settings
Fix broken library subtitle download settings
2026-02-28 10:08:20 +01:00
MBR#0001 33496c1693 Fix broken library subtitle download settings 2026-02-26 19:54:24 +01:00
Bond-009 b65daeca0b Merge pull request #16150 from dfederm/nullref-season-series
Fix nullref in Season.GetEpisodes when the season is detached from a series
2026-02-22 13:49:41 +01:00
David Federman 286cc6d720 Fix nullref in Season.GetEpisodes when the season is detached from a series 2026-02-20 22:56:30 -08:00
Andrew Rabert aa4f09c799 Mitigate pull_request_target privilege escalation
Hotfix — replaces pull_request_target with pull_request to stop
granting write permissions and secrets to fork PRs. Some workflows
will break; can be fixed properly later.
2026-02-20 19:10:40 -05:00
rijads afd3c0d9f3 Fix subtitle extraxtion caching empty files 2026-02-19 17:53:47 +01:00
theguymadmax 5597d8e1a7 Checkpoint wal 2026-02-18 15:11:57 -05:00
theguymadmax 0166362258 Use BackupDatabase() instead of File.Move in library.db migration 2026-02-17 22:56:45 -05:00
Bond-009 58c330b63d Merge pull request #16226 from dfederm/fix/16134-migration-unique-constraint
Deduplicate provider IDs during MigrateLibraryDb migration
2026-02-14 11:46:43 +01:00
Bond-009 be71295693 Merge pull request #16227 from dfederm/fix/16149-watch-state-episode-replace
Reattach user data after item removal during library scan
2026-02-14 11:45:51 +01:00
Bond-009 8cd3090cee Merge pull request #16231 from theguymadmax/skip-image-for-empty-folders
Skip image checks for empty folders
2026-02-14 11:43:24 +01:00
David Federman 7bf08daeec Reattach user data after removing items during library scan
When items are removed during a library scan, their user data is
detached to a placeholder. If a replacement item already exists
(e.g., a new version of the same episode was added before the old
file was deleted), the user data would be stranded in the placeholder
because the replacement item's initial ReattachUserDataAsync call
happened before the old item was deleted.

This fix checks for remaining valid children that share user data
keys with removed items and reattaches any detached user data to them.

Fixes #16149
2026-02-12 20:38:28 -08:00
David Federman 290463fe7b Fix migration UNIQUE constraint on BaseItemProviders
Deduplicate ProviderIds by ProviderId during MigrateLibraryDb migration
to prevent UNIQUE constraint violations when legacy data contains
duplicate provider entries for the same item.

Fixes #16134
2026-02-12 20:37:26 -08:00
theguymadmax 1b2d9c100a Skip image checks for empty folders 2026-02-12 18:04:27 -05:00
Bond-009 caa05c1bf2 Merge pull request #16116 from saltpi/bugfix
Fix TMDB image URLs missing size parameter
2026-02-02 20:48:21 +01:00
Niels van Velzen a37ead86df Merge pull request #16098 from theguymadmax/fix-random-sort
Fix random sort returning duplicate items
2026-01-27 11:31:41 +01:00
Niels van Velzen e65aff8bc6 Merge pull request #16109 from nielsvanvelzen/ws-session-info-dto-backport
Fix SessionInfoWebSocketListener not using SessionInfoDto
2026-01-27 11:31:15 +01:00
endpne 9734494eb6 Fix TMDB image URLs missing size parameter 2026-01-26 13:25:03 +08:00
Niels van Velzen d41e302418 Fix SessionInfoWebSocketListener not using SessionInfoDto 2026-01-25 21:21:48 +01:00
theguymadmax 80ba517294 Fix random sort returning duplicate items 2026-01-24 13:48:05 -05:00
MarcoCoreDuo 95d08b264f Rehydrate cached UserData after reattachment (#16071) 2026-01-22 17:43:05 -07:00
IceStormNG 893a849f28 Slightly adjust segment length for fractional framerates (#16053)
Co-authored-by: Carsten Braun <carsten.braun@braun-cloud.de>
2026-01-22 17:41:51 -07:00
theguymadmax 673f617994 Fix TMDB crew department mapping (#16066) 2026-01-22 17:40:35 -07:00
theguymadmax 644327eb76 Revert hidden directory ignore pattern (#16077) 2026-01-22 17:39:55 -07:00
Jellyfin Release Bot 10662e75e4 Bump version to 10.11.6 2026-01-18 20:02:59 -05:00
Joshua M. Boniface a2b1936e73 Merge pull request #15816 from theguymadmax/preserve-artist-order
Fix artist display order
2026-01-18 19:48:17 -05:00
theguymadmax 2df546af6d Deduplicate using Distinct 2026-01-18 18:16:45 -05:00
Claus Vium 338b480217 Merge pull request #16046 from theguymadmax/restore-weekly-images
Restore weekly refresh for library folder images
2026-01-18 16:36:45 +01:00
theguymadmax 2943bb6fdd Restore collection folder image refresh 2026-01-18 01:51:51 -05:00
theguymadmax 94edcbd2d1 Fix artist ordering DtoServices 2026-01-17 10:14:41 -05:00
theguymadmax a8d1cdefac Address review comments 2026-01-17 10:14:41 -05:00
Tim Eisele a518160a6f Prioritize better matches on search (#15983) 2026-01-16 19:05:46 -07:00
Tim Eisele b56de6493f Be more strict about PersonType assignments (#15872) 2026-01-16 19:03:13 -07:00
theguymadmax 093cfc3f3b Trim music artist names (#15808) 2026-01-16 18:51:48 -07:00
theguymadmax 49775b1f6a Fix birthplace not saving correctly (#16020) 2026-01-16 18:47:40 -07:00
Collin T Swisher 22d593b8e9 Add mblink creation logic to library update endpoint. (#15965) 2026-01-16 18:47:04 -07:00
theguymadmax 2cb7fb52d2 Skip hidden directories and .ignore paths in library monitoring (#16029) 2026-01-16 18:45:19 -07:00
Joshua M. Boniface 8433b6d8a4 Merge pull request #15899 from MarcoCoreDuo/fix-watch-state-not-kept
Fix watched state not kept on Media replace/rename
2026-01-16 16:40:36 -05:00
Bond-009 32d2414de0 Merge pull request #15950 from theguymadmax/revert-sort-index-number
Revert "always sort season by index number"
2026-01-09 18:38:23 +01:00
Bond-009 317a3a47c3 Merge pull request #15961 from theguymadmax/fix-bad-plugin-url
Fix crash when plugin repository has an invalid URL
2026-01-09 18:24:48 +01:00
theguymadmax 845b8cdc8f Fix crash when plugin repository has an invalid URL 2026-01-06 11:57:25 -05:00
theguymadmax c86f6439c5 Revert "always sort season by index number"
This reverts commit e16ea7b236.
2026-01-05 11:06:25 -05:00
theguymadmax 559e0088e5 Fix tag inheritance for Continue Watching queries (#15931) 2026-01-04 11:20:34 -07:00
MarcoCoreDuo adaca95590 make db context creation async 2025-12-31 07:43:07 +01:00
MarcoCoreDuo 09a1c31fa3 Refactor ReattachUserData methods to be asynchronous 2025-12-31 03:06:07 +01:00
MarcoCoreDuo e4b82025b8 move reattaching user data to own function and call it only after fetching metadata for the first time 2025-12-30 22:04:59 +01:00
Collin T Swisher 78e3702cb0 Fix playlist item de-duplication (#15858) 2025-12-24 07:50:15 -07:00
Bond-009 01b20d3b75 Merge pull request #15833 from nyanmisaka/fix-h264-av1-sdr-hls-fallback
Fix missing H.264 and AV1 SDR fallbacks in HLS playlist
2025-12-24 10:28:33 +01:00
Tim Eisele 156761405e Prefer US rating on fallback (#15793) 2025-12-19 20:41:09 -07:00
Claus Vium 1805f2259f add CultureDto cache (#15826) 2025-12-19 20:38:54 -07:00
Nyanmisaka 4c587776d6 Fix the use of HWA in unsupported H.264 Hi422P/Hi444PP (#15819) 2025-12-19 19:58:56 -07:00
gnattu 8379b4634a Enforce more strict webm check (#15807) 2025-12-19 19:57:08 -07:00
Nyanmisaka 9470439cfa Fix video lacking SAR and DAR are marked as anamorphic (#15834) 2025-12-19 19:54:48 -07:00
gnattu 18096e48e0 Use hvc1 codectag for Dolby Vision 8.4 (#15835) 2025-12-19 19:53:28 -07:00
nyanmisaka f2d0ac7b28 Fix missing H.264 and AV1 SDR fallbacks in HLS playlist
Previously, if HEVC encoding was disabled on the server,
SDR fallbacks would not be provided.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2025-12-19 20:33:24 +08:00
theguymadmax 2ccf08f547 Fix artist display order 2025-12-17 01:09:13 -05:00
Jellyfin Release Bot 1e27f460fe Bump version to 10.11.5 2025-12-14 21:44:14 -05:00
Andrew Rabert 4cdd8c8233 Fix unnecessary database JOINs in ApplyNavigations (#15666) 2025-12-13 10:58:08 -07:00
Tim Eisele 6e60634c9f Skip invalid ignore rules (#15746) 2025-12-13 08:39:49 -07:00
theguymadmax 12c5d6b636 Fix backdrop images being deleted when stored with media (#15766) 2025-12-13 08:29:17 -07:00
theguymadmax b617c62f8e Fix NullReferenceException in ApplyOrder method (#15768) 2025-12-13 08:28:31 -07:00
Nyanmisaka 035b5895b0 Fix AV1 decoding hang regression on RK3588 (#15776) 2025-12-13 08:27:29 -07:00
theguymadmax 22da5187c8 Fix collection display order (#15767) 2025-12-13 08:27:01 -07:00
theguymadmax 5804d6840c Fix parental rating comparison with sub-scores (#15786) 2025-12-13 08:25:48 -07:00
Bond-009 b50ce1ad6b Merge pull request #15752 from Collin-Swish/fix-name-case-insensitivity
Fix case sensitivity edge case
2025-12-12 21:39:22 +01:00
Bond-009 481ee03f35 Merge pull request #15757 from theguymadmax/fix-trickplays-for-alt-versions
Fix trickplay images using wrong item on alternate versions
2025-12-12 21:31:52 +01:00
Bond-009 d91adb5d54 Merge pull request #15662 from SapientGuardian/issue15661
Fix blocking in async context in LimitedConcurrencyLibraryScheduler
2025-12-10 20:37:57 +01:00
theguymadmax ef7f138a4e Fix trickplay images using wrong item on alternate versions 2025-12-09 14:21:09 -05:00
Collin Swisher 2e8d9a311b Fix case sensitivity edge case 2025-12-08 17:41:48 -06:00
gnattu 4c5a3fbff3 Use original name for MusicAritist matching (#15689) 2025-12-05 19:30:02 -07:00
liszto 636908fc4d Fix thumbnails not being deleted from temp folder 2025-12-05 19:29:54 -07:00
Tim Eisele 997362fc97 Backport dependency updates (#15723) 2025-12-05 19:27:30 -07:00
Noah Potash c5147341e3 Fixes 15661. Replace BlockingCollection with Channel in LimitedConcurrencyLibraryScheduler to prevent blocking in an asynchronous context. 2025-12-03 21:50:08 -05:00
Noah Potash ca33bcebf0 Add SapientGuardian to CONTRIBUTORS.md 2025-12-03 21:27:26 -05:00
Ivan Kara d32f487e8e Fix symlinked file size (#15681) 2025-12-03 19:04:59 -07:00
theguymadmax fb65f8f853 Fix ItemAdded event triggering when updating metadata (#15680) 2025-12-03 19:02:55 -07:00
martenumberto 2a0b90e385 Fix: Add .ts fallback for video streams to prevent crash (#15690) 2025-12-03 19:02:39 -07:00
myzhysz dde70fd8a2 Fix stack overflow while scanning (#15698) 2025-12-03 19:02:04 -07:00
Niels van Velzen 98d1d0cb35 Merge pull request #15670 from nyanmisaka/fix-mjpeg-rk3576
Fix the empty output of trickplay on RK3576
2025-12-02 13:48:51 +01:00
Jellyfin Release Bot ba76a8f3ad Bump version to 10.11.4 2025-11-30 21:33:32 -05:00
Anthony Lavado 8cd5652157 Merge pull request #15672 from jellyfin/openapi-cache-z
Cache OpenApi document generation
2025-11-30 21:22:29 -05:00
crobibero 8aff4227d9 Implement caching for OpenAPI document 2025-11-30 09:19:19 -07:00
nyanmisaka 026f7472cb Fix the empty output of trickplay on RK3576
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2025-11-30 21:38:47 +08:00
MBR-0001 daca285568 Revert "Localization/iso6392.txt: change pob and pop" (#15555) 2025-11-23 19:20:29 +01:00
theguymadmax fbb9a0b2c7 Fix ResolveLinkTarget crashing on exFAT drives (#15568) 2025-11-21 21:14:39 -07:00
Ziyuan Qu 29b3aa8543 Add hidden file check in bdInfo (#15582) 2025-11-21 21:14:30 -07:00
theguymadmax 94f3725208 Fix isMovie filter logic (#15594) 2025-11-21 21:14:03 -07:00
theguymadmax 0ee81e87be Fix locked fields on not saving (#15564) 2025-11-19 17:02:53 +01:00
theguymadmax c491a918c2 Save item to database before providers run to prevent FK constraint errors (#15563) 2025-11-19 17:01:13 +01:00
gnattu 1e7e46cb82 Prevent copying HDR streams when only SDR is supported (#15556) 2025-11-18 18:37:35 -07:00
theguymadmax 5ae444d96d Fix NullReferenceException in filesystem path comparison (#15548) 2025-11-18 18:37:09 -07:00
gnattu ee7ad83427 Restrict first video frame probing to file protocol (#15557) 2025-11-18 18:36:59 -07:00
Jellyfin Release Bot 921d7d3364 Bump version to 10.11.3 2025-11-16 17:40:07 -05:00
theguymadmax f8e012582a Fix movie titles using folder name when NFOs saver is enabled (#15529) 2025-11-16 13:59:58 -07:00
theguymadmax def5956cd1 Fix tmdbid not detected in single movie folder (#14955) 2025-11-16 13:36:35 -07:00
theguymadmax abfbaca336 Fix series DateLastMediaAdded not updating when new episodes are added (#15472) 2025-11-16 13:35:43 -07:00
theguymadmax 6566188e45 Add 1 minute tolerance for NFO change detection (#15514) 2025-11-15 08:39:25 -07:00
theguymadmax 078f9584ed Fix playlist DateCreated and DateLastMediaAdded not being set (#15508) 2025-11-14 15:19:40 -07:00
Iksas ee34c75386 fix missing font extraction for certain transcoding settings (#15502) 2025-11-13 18:30:18 -07:00
theguymadmax e8150428b6 Fix .ignore handling for directories (#15501) 2025-11-13 18:23:18 -07:00
theguymadmax 4b38e35bbb Remove InheritedTags and update tag filtering logic (#15493) 2025-11-13 18:23:03 -07:00
Huo Jiacheng 435bb14bb2 Fix gitignore-style not working properly on windows. (#15487) 2025-11-12 19:43:13 -07:00
theguymadmax 2e5ced5098 Improve season folder parsing (#15404) 2025-11-12 17:36:57 -07:00
Bond-009 f4a846aa4d Don't error out when searching for marker files fails (#15466)
Fixes #15445
2025-11-11 15:45:47 -07:00
Joshua M. Boniface 7c1063177f Merge pull request #15462 from theguymadmax/fix-exception-for-empty-strm-files
Fix NullReferenceException in GetPathProtocol when path is null
2025-11-10 19:30:38 -05:00
Joshua M. Boniface 5878b1ffc5 Merge pull request #15468 from Bond-009/carefulWithLastMinChanges
Check if target exists before trying to follow it
2025-11-10 19:12:24 -05:00
Bond_009 3c3c2aee0d Check if target exists before trying to follow it
Exception got caught in ManagedFileSystem and wrong file info got returned
2025-11-10 23:19:17 +01:00
theguymadmax 511223aac4 Fix NullReferenceException in GetPathProtocol when path is null 2025-11-10 02:30:49 -05:00
Mikal S. 3b2d64995a Resolve symlinks for static media source infos (#15263) 2025-11-09 09:45:02 -07:00
theguymadmax 13c4517a66 Fix collection grouping in mixed libraries (#15373) 2025-11-09 09:35:50 -07:00
theguymadmax 177b6464ca Don't clear baseitemids (#15446) 2025-11-09 09:22:09 -07:00
Bond-009 5a9a8363f4 Merge pull request #15441 from IceStormNG/fix-nullreference-role-null-10.11
Fix System.NullReferenceException when people's role is null (10.11.z)
2025-11-08 18:25:03 +01:00
theguymadmax 49efd68fc7 Invalidate parent folder's cache on deletion/creation (#15423) 2025-11-08 08:30:04 -07:00
Carsten Braun 90a8a26c6e Copy-Pasting is sometimes hard.... 2025-11-08 15:00:11 +01:00
Carsten Braun 002c83e6f5 Fix NullReferenceExceltop when role is null. 2025-11-08 14:32:14 +01:00
theguymadmax 7222910b05 Fix filters to use SortName (#15381) 2025-11-07 18:21:41 -07:00
Bond-009 097cb87f6f Don't enforce a minimum amount of free space for the tmp and log dirs (#15390) 2025-11-07 18:21:10 -07:00
JPVenson 91c3b1617e Fixed missing sort argument (#15413) 2025-11-07 18:20:42 -07:00
theguymadmax 8f71922734 Fix item count display for collapsed items (#15380) 2025-11-07 18:20:10 -07:00
Niels van Velzen d140630208 Update branding in Swagger page (#15422) 2025-11-07 18:19:30 -07:00
theguymadmax 63a3e55297 Fix search terms using diacritics (#15435) 2025-11-07 18:18:24 -07:00
evanreichard c2e5081d64 feat(sqlite): add timeout config (#15369) 2025-11-07 18:17:43 -07:00
Jellyfin Release Bot 4187c6f620 Bump version to 10.11.2 2025-11-02 21:28:56 -05:00
Tim Eisele e7dbb3afec Skip too large extracted season numbers (#15326) 2025-11-02 09:11:48 -07:00
vinnyspb f994dd6211 Update file size when refreshing metadata (#15325) 2025-11-01 14:18:19 -06:00
Cody Robibero da254ee968 return instead of break, add check to more migrations (#15322) 2025-11-01 14:17:22 -06:00
Bill Thornton 4ad3141875 Update password reset to always return the same response structure (#15254) 2025-11-01 14:17:09 -06:00
evanreichard b5f0199a25 fix: in optimistic locking, key off table is locked (#15328) 2025-11-01 14:15:26 -06:00
Nyanmisaka 6bf88c049e Ignore initial delay in audio-only containers (#15247) 2025-10-29 20:40:28 -06:00
Jellyfin Release Bot 40a33da2a5 Bump version to 10.11.1 2025-10-26 22:02:09 -04:00
Joshua M. Boniface 3596fc0693 Fix bump_version to handle spaced filename 2025-10-26 21:50:38 -04:00
Jellyfin Release Bot 93824dad97 Bump version to 10.11.1 2025-10-26 21:41:27 -04:00
Tim Eisele e5656af1f2 Improve symlink handling (#15209) 2025-10-26 15:10:13 -06:00
Niels van Velzen c127c10458 Merge pull request #15225 from Bond-009/z440ATL
Update dependency z440.atl.core to 7.6.0
2025-10-26 18:50:04 +01:00
Tim Eisele 7d1824ea27 Fix pagination and sorting for folders (#15187) 2025-10-26 11:34:11 -06:00
Cody Robibero 2966d27c97 Skip invalid database migration (#15212) 2025-10-26 11:34:04 -06:00
Ivan Kara 618ec4543e Add season number fallback for OMDB and TMDB plugins (#15113) 2025-10-26 11:33:55 -06:00
Cody Robibero 0e4031ae52 Skip extracting directory entry when restoring (#15196) 2025-10-26 11:33:47 -06:00
CeruleanRed 442af96ed9 Only save chapters that are within the runtime of the video file (#15176) 2025-10-26 10:37:16 -06:00
JJBlue a305204cfa Skip extracted files in migration if bad timestamp or no access (#15220)
Fixes #15024
2025-10-26 10:30:43 -06:00
theguymadmax 75f472e6a7 Normalize paths in database queries (#15217) 2025-10-26 10:30:12 -06:00
Bond_009 cc32e8f7cb Update dependency z440.atl.core to 7.6.0 2025-10-26 15:16:08 +01:00
MBR-0001 14b3085ff1 Fix Has(Imdb/Tmdb/Tvdb)Id checks (#15126) 2025-10-25 16:00:55 -06:00
Cody Robibero 5691eee4f1 Prefer filting by package id instead of name (#15197) 2025-10-25 09:37:09 -06:00
theguymadmax 1520a697ad Play selected song first with instant mix (#15133) 2025-10-25 09:33:11 -06:00
Cody Robibero 81b8b0ca4a Add the transcode marker during startup instead of first transcode (#15194) 2025-10-25 09:32:15 -06:00
Cody Robibero ac3fa3c376 Clean up backup service (#15170) 2025-10-24 17:57:34 -06:00
Tim Eisele 7a1c1cd342 Skip extracted files in migration if bad timestamp or no access (#15112) 2025-10-24 17:57:19 -06:00
gnattu 70c32a26fa Make priority class setting more robust (#15177) 2025-10-24 17:57:02 -06:00
Cody Robibero 2b94bb54aa Fix xml formatter (#15164) 2025-10-24 17:56:38 -06:00
Bond-009 0a6e8146be Lower required tmp dir size to 512MiB (#15098) 2025-10-23 16:38:27 -06:00
theguymadmax 305b0fdca3 Make season paths case-insensitive (#15102) 2025-10-23 16:38:06 -06:00
theguymadmax d738386fe2 Fix LiveTV images not saving to database (#15083) 2025-10-23 16:37:55 -06:00
Tim Eisele ca830d5be7 Speed-up trickplay migration (#15054) 2025-10-23 16:37:47 -06:00
theguymadmax a5bc4524d8 Optimize artist query (#15087) 2025-10-23 16:37:29 -06:00
Nyanmisaka 175ee12bbc Fix videos with cropping metadata are probed as anamorphic (#15144) 2025-10-23 16:31:11 -06:00
Nyanmisaka a725220c21 Reject stream copy of HDR10+ video if the client does not support HDR10 (#15072) 2025-10-21 17:20:56 -06:00
gnattu a245605152 Log the message more clear when network manager is not ready (#15055) 2025-10-21 17:18:26 -06:00
Tim Eisele f4a53209f4 Skip invalid keyframe cache data (#15032) 2025-10-21 17:17:56 -06:00
Jellyfin Release Bot 877251bcae Bump version to 10.11.0 2025-10-19 20:45:12 -04:00
634 changed files with 57667 additions and 5128 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.9",
"version": "10.0.11",
"commands": [
"dotnet-ef"
]
-44
View File
@@ -1,44 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
schedule:
- cron: '24 2 * * 4'
permissions:
contents: read
security-events: write
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'csharp' ]
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
-159
View File
@@ -1,159 +0,0 @@
name: ABI Compatibility
on:
pull_request:
permissions: {}
jobs:
abi-head:
name: ABI - HEAD
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
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
with:
dotnet-version: '10.0.x'
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: abi-head
retention-days: 14
if-no-files-found: error
path: out/
abi-base:
name: ABI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
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
with:
dotnet-version: '10.0.x'
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: abi-base
retention-days: 14
if-no-files-found: error
path: out/
abi-diff:
permissions:
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
name: ABI - Difference
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
needs:
- abi-head
- abi-base
steps:
- name: Download abi-head
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: abi-head
path: abi-head
- name: Download abi-base
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: abi-base
path: abi-base
- name: Setup ApiCompat
run: |
dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool
- name: Run ApiCompat
id: diff
run: |
{
echo 'body<<EOF'
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 || true )"
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
printf "\n${file}\n${COMPAT_OUTPUT}\n"
fi
done
echo EOF
} >> $GITHUB_OUTPUT
- name: Find difference comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: abi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
token: ${{ secrets.JF_BOT_TOKEN }}
body: |
<!--abi-diff-workflow-comment-->
<details>
<summary>ABI Difference</summary>
```
${{ steps.diff.outputs.body }}
```
</details>
- name: Reply or edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
token: ${{ secrets.JF_BOT_TOKEN }}
body: |
<!--abi-diff-workflow-comment-->
<details>
<summary>ABI Difference</summary>
No changes to the ABI found. See history of this comment for previous changes.
</details>
-25
View File
@@ -1,25 +0,0 @@
name: Format
on:
push:
branches:
- master
# Run formatter against the forked branch, but
# do not allow access to secrets
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories
pull_request:
env:
SDK_VERSION: "10.0.x"
jobs:
format-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
- name: Run DotNet Format
run: dotnet format --verify-no-changes --verbosity minimal
-45
View File
@@ -1,45 +0,0 @@
name: Tests
on:
push:
branches:
- master
# Run tests against the forked branch, but
# do not allow access to secrets
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories
pull_request:
env:
SDK_VERSION: "10.0.x"
jobs:
run-tests:
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
fail-fast: false
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
- name: Run DotNet CLI Tests
run: >
dotnet test Jellyfin.sln
--configuration Release
--collect:"XPlat Code Coverage"
--settings tests/coverletArgs.runsettings
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
reporttypes: "Cobertura"
# TODO - which action / tool to use to publish code coverage results?
# - name: Publish code coverage results
-63
View File
@@ -1,63 +0,0 @@
name: Commands
on:
issue_comment:
types:
- created
- edited
pull_request:
types:
- labeled
- synchronize
permissions: {}
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
rename:
name: Rename
if: contains(github.event.comment.body, '@jellyfin-bot rename')
runs-on: ubuntu-latest
steps:
- name: pull in script
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.14'
cache: 'pip'
- name: install python packages
run: pip install -r rename/requirements.txt
- name: run rename script
run: python3 rename.py
working-directory: ./rename
env:
GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
GH_REPO: ${{ github.repository }}
ISSUE: ${{ github.event.issue.number }}
COMMENT_ID: ${{ github.event.comment.id }}
-35
View File
@@ -1,35 +0,0 @@
name: Stale Issue Labeler
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
actions: write
jobs:
issues:
name: Check for stale issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
operations-per-run: 500
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
close-issue-message: |-
This issue was closed due to inactivity.
@@ -1,32 +0,0 @@
name: Check Issue Template
on:
issues:
types:
- opened
jobs:
check_issue:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: pull in script
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.14'
cache: 'pip'
- name: install python packages
run: pip install -r main-repo-triage/requirements.txt
- name: check and comment issue
working-directory: ./main-repo-triage
run: python3 single_issue_gha.py
env:
GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
GH_REPO: ${{ github.repository }}
ISSUE: ${{ github.event.issue.number }}
-44
View File
@@ -1,44 +0,0 @@
name: OpenAPI Generate
on:
workflow_call:
inputs:
ref:
required: true
type: string
repository:
required: true
type: string
artifact:
required: true
type: string
permissions:
contents: read
jobs:
main:
name: Main
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref }}
repository: ${{ inputs.repository }}
- name: Configure .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: '10.0.x'
- name: Create File
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests
- name: Upload Artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.artifact }}
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
retention-days: 14
if-no-files-found: error
-140
View File
@@ -1,140 +0,0 @@
name: OpenAPI Publish
on:
push:
branches:
- master
tags:
- 'v*'
jobs:
publish-openapi:
name: OpenAPI - Publish Artifact
uses: ./.github/workflows/openapi-generate.yml
permissions:
contents: read
with:
ref: ${{ github.sha }}
repository: ${{ github.repository }}
artifact: openapi-head
publish-unstable:
name: OpenAPI - Publish Unstable Spec
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- publish-openapi
steps:
- name: Set unstable dated version
id: version
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (unstable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/unstable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-unstable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
# Move current jellyfin-openapi-unstable.json symlink to jellyfin-openapi-unstable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-unstable.json ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
# Create new jellyfin-openapi-unstable.json symlink
sudo ln -s unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-unstable.json
# Check that the previous openapi unstable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-unstable_previous.json )" != "unstable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
fi
) 200>/run/workflows/openapi-unstable.lock
publish-stable:
name: OpenAPI - Publish Stable Spec
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- publish-openapi
steps:
- name: Set version number
id: version
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (stable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-stable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Create new jellyfin-openapi-stable.json symlink
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
# Check that the previous openapi stable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
fi
) 200>/run/workflows/openapi-stable.lock
@@ -1,80 +0,0 @@
name: OpenAPI Check
on:
pull_request:
jobs:
ancestor:
name: Common Ancestor
runs-on: ubuntu-latest
outputs:
base_ref: ${{ steps.ancestor.outputs.base_ref }}
steps:
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Search History
id: ancestor
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} HEAD)
echo "ref: ${ANCESTOR_REF}"
echo "base_ref=${ANCESTOR_REF}" >> "$GITHUB_OUTPUT"
head:
name: Head Artifact
uses: ./.github/workflows/openapi-generate.yml
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
artifact: openapi-head
base:
name: Base Artifact
uses: ./.github/workflows/openapi-generate.yml
needs:
- ancestor
with:
ref: ${{ needs.ancestor.outputs.base_ref }}
repository: ${{ github.event.pull_request.base.repo.full_name }}
artifact: openapi-base
diff:
name: Generate Report
runs-on: ubuntu-latest
needs:
- head
- base
steps:
- name: Download Head
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-head
path: openapi-head
- name: Download Base
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-base
path: openapi-base
- name: Detect Changes
id: openapi-diff
run: |
sed -i 's:allOf:oneOf:g' openapi-head/openapi.json
sed -i 's:allOf:oneOf:g' openapi-base/openapi.json
mkdir -p /tmp/openapi-report
mv openapi-head/openapi.json /tmp/openapi-report/head.json
mv openapi-base/openapi.json /tmp/openapi-report/base.json
docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md
- name: Upload Artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: openapi-report
path: /tmp/openapi-report/openapi-report.md
@@ -1,59 +0,0 @@
name: OpenAPI Report
on:
workflow_run:
workflows:
- OpenAPI Check
types:
- completed
jobs:
metadata:
name: Generate Metadata
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
pr_number: ${{ steps.pr_number.outputs.pr_number }}
steps:
- name: Get Pull Request Number
id: pr_number
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
API_RESPONSE=$(gh pr list --repo "${GITHUB_REPOSITORY}" --search "${HEAD_SHA}" --state open --json number)
PR_NUMBER=$(echo "${API_RESPONSE}" | jq '.[0].number')
echo "repository: ${GITHUB_REPOSITORY}"
echo "sha: ${HEAD_SHA}"
echo "response: ${API_RESPONSE}"
echo "pr: ${PR_NUMBER}"
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
comment:
name: Pull Request Comment
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
needs:
- metadata
permissions:
pull-requests: write
actions: read
contents: read
steps:
- name: Download OpenAPI Report
id: download_report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-report
path: openapi-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Push Comment
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
with:
github-token: ${{ secrets.JF_BOT_TOKEN }}
file-path: ${{ steps.download_report.outputs.download-path }}/openapi-report.md
pr-number: ${{ needs.metadata.outputs.pr_number }}
comment-tag: openapi-report
-66
View File
@@ -1,66 +0,0 @@
name: Project Automation
on:
push:
branches:
- master
pull_request:
issue_comment:
permissions: {}
jobs:
project:
name: Project board
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Remove from 'Current Release' project
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
project: Current Release
action: delete
column: In progress
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true
with:
project: Release Next
column: In progress
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
project: Current Release
column: In progress
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Check number of comments from the team member
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
id: member_comments
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
- name: Move issue to needs triage
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true
with:
project: Issue Triage for Main Repo
column: Needs triage
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true
with:
project: Issue Triage for Main Repo
column: Pending response
repo-token: ${{ secrets.JF_BOT_TOKEN }}
@@ -1,24 +0,0 @@
name: Merge Conflict Labeler
on:
push:
branches:
- master
pull_request_target:
types: [synchronize]
permissions: {}
jobs:
main:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
with:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: ${{ secrets.JF_BOT_TOKEN }}
-30
View File
@@ -1,30 +0,0 @@
name: Stale PR Check
on:
schedule:
- cron: '30 */12 * * *'
workflow_dispatch:
permissions:
pull-requests: write
actions: write
jobs:
prs-stale-conflicts:
name: Check PRs with merge conflicts
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
operations-per-run: 150
# The merge conflict action will remove the label when updated
remove-stale-when-updated: false
days-before-stale: -1
days-before-close: 90
days-before-issue-close: -1
stale-pr-label: merge conflict
close-pr-message: |-
This PR has been closed due to having unresolved merge conflicts.
@@ -1,82 +0,0 @@
name: '🆙 Auto bump_version'
on:
release:
types:
- published
workflow_dispatch:
inputs:
TAG_BRANCH:
required: true
description: release-x.y.z
NEXT_VERSION:
required: true
description: x.y.z
jobs:
auto_bump_version:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }}
env:
TAG_BRANCH: ${{ github.event.release.target_commitish }}
steps:
- name: Wait for deploy checks to finish
uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1
with:
ref: ${{ env.TAG_BRANCH }}
intervalSeconds: 60
timeoutSeconds: 3600
- name: Setup YQ
uses: chrisdickinson/setup-yq@fa3192edd79d6eb0e4e12de8dde3a0c26f2b853b # latest
with:
yq-version: v4.9.8
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ env.TAG_BRANCH }}
- name: Setup EnvVars
run: |-
CURRENT_VERSION=$(yq e '.version' build.yaml)
CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*}
CURRENT_PATCH=${CURRENT_VERSION##*.}
echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV
echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV
echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV
echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV
- name: Run bump_version
run: ./bump_version ${{ env.NEXT_VERSION }}
- name: Commit Changes
run: |-
git config user.name "jellyfin-bot"
git config user.email "team@jellyfin.org"
git checkout ${{ env.TAG_BRANCH }}
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
git push origin ${{ env.TAG_BRANCH }}
manual_bump_version:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }}
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ env.TAG_BRANCH }}
- name: Run bump_version
run: ./bump_version ${{ env.NEXT_VERSION }}
- name: Commit Changes
run: |-
git config user.name "jellyfin-bot"
git config user.email "team@jellyfin.org"
git checkout ${{ env.TAG_BRANCH }}
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
git push origin ${{ env.TAG_BRANCH }}
+79
View File
@@ -0,0 +1,79 @@
when:
- event: pull_request
- event: push
steps:
# Restore, build and test in one step so the NuGet cache stays on the step's
# own ephemeral storage instead of the 10Gi workspace volume.
# global.json pins the .NET 10 SDK (10.0.0, rollForward latestMinor).
- name: build-test
image: mcr.microsoft.com/dotnet/sdk:10.0
environment:
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
DOTNET_NOLOGO: "1"
commands:
- dotnet --info
- dotnet restore Jellyfin.sln
- dotnet build Jellyfin.sln -c Release --no-restore
# libSkiaSharp needs fontconfig to load and the SDK image does not ship it.
# The mirror occasionally serves a half-synced index, so update is retried.
- apt-get -o Acquire::Retries=3 update || apt-get -o Acquire::Retries=3 update
- apt-get install -y --no-install-recommends libfontconfig1
- ldconfig -p | grep -q libfontconfig
# BackupServiceTests is excluded because BackupService refuses to write a
# backup with less than 5GiB free on its target path. The clone and build
# output fill 5.7G of the 9.8G workspace volume, leaving 4.1G. Raising the
# agent's workspace volume size is the only way to run it here.
- df -h .
- dotnet test Jellyfin.sln -c Release --no-build --verbosity minimal --filter "Category!=RequiresDocker&FullyQualifiedName!~Integration&FullyQualifiedName!~BackupServiceTests"
backend_options:
kubernetes:
serviceAccountName: jellyfin-ha-src
resources:
requests:
memory: 2Gi
cpu: 2
ephemeral-storage: 10Gi
limits:
memory: 8Gi
cpu: 4
ephemeral-storage: 20Gi
# The PostgreSQL migration tests are the only ones that run the startup migration chain against
# the provider production uses, and the filter above has always skipped them.
# The server runs inside this step: the kubernetes backend has no docker daemon for
# testcontainers, and a postgres service container deadlocks the step because the backend mounts
# the ReadWriteOnce workspace volume into service pods and schedules them on another node.
# Its data directory lives on the step's ephemeral storage, not on the workspace volume.
# Scoped to Jellyfin.Server.Tests: the three classes in Jellyfin.Database.Tests.PostgreSQL still
# start their own container, and PostgreSqlProviderTests already fails on main - on an EF 10
# scalar query and on its own data - which a third test in the class then inherits.
- name: postgres-migration-chain
image: mcr.microsoft.com/dotnet/sdk:10.0
depends_on:
- build-test
environment:
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
DOTNET_NOLOGO: "1"
JELLYFIN_TEST_POSTGRES: "Host=127.0.0.1;Port=5432;Database=postgres;Username=postgres"
commands:
- apt-get -o Acquire::Retries=3 update || apt-get -o Acquire::Retries=3 update
- DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends postgresql
- install -d -o postgres -g postgres /tmp/pgdata /tmp/pgrun
- PGBIN=$(ls -d /usr/lib/postgresql/*/bin | tail -1)
- su postgres -c "$PGBIN/initdb -D /tmp/pgdata -A trust -U postgres"
- su postgres -c "$PGBIN/pg_ctl -D /tmp/pgdata -o \"-c listen_addresses=127.0.0.1 -k /tmp/pgrun\" -l /tmp/pg.log -w start"
- dotnet build tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj -c Release
- dotnet test tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj -c Release --no-build --verbosity minimal --filter "Category=RequiresDocker"
backend_options:
kubernetes:
serviceAccountName: jellyfin-ha-src
resources:
requests:
memory: 2Gi
cpu: 2
ephemeral-storage: 6Gi
limits:
memory: 6Gi
cpu: 4
ephemeral-storage: 12Gi
+9 -1
View File
@@ -143,6 +143,7 @@
- [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
- [st7105](https://github.com/st7105)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
- [ssenart](https://github.com/ssenart)
@@ -172,6 +173,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 +235,12 @@
- [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)
- [m0g3r](https://github.com/m0g3r)
- [martin-77](https://github.com/martin-77)
- [Oggeb1](https://github.com/Oggeb1)
# Emby Contributors
@@ -243,7 +251,7 @@
- [Mark2xv](https://github.com/Mark2xv)
- [ScottRapsey](https://github.com/ScottRapsey)
- [skynet600](https://github.com/skynet600)
- [Cheesegeezer](https://githum.com/Cheesegeezer)
- [Cheesegeezer](https://github.com/Cheesegeezer)
- [Radeon](https://github.com/radeonorama)
- [gcw07](https://github.com/gcw07)
- [SivaramAdhiappan](https://github.com/shivaram1190)
+36 -28
View File
@@ -8,8 +8,9 @@
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit3" Version="4.19.0" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="AWSSDK.S3" Version="4.0.103.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.6.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.6.1" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
@@ -17,7 +18,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 +27,37 @@
<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.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.11" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="1.1.0.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Npgsql" Version="10.0.3" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<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,21 +72,24 @@
<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" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StackExchange.Redis" Version="2.13.17" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<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.11" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.15.3" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.15.0" />
<PackageVersion Include="Testcontainers.Redis" Version="4.15.0" />
<PackageVersion Include="z440.atl.core" Version="7.16.0" />
<PackageVersion Include="TMDbLib" Version="3.0.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="UTF.Unknown" Version="2.7.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="Xunit.v3.Priority" Version="1.1.18" />
+96
View File
@@ -0,0 +1,96 @@
# syntax=docker/dockerfile:1
# ── Build stage ──────────────────────────────────────────────────────────────
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Restore dependencies first (layer-cache friendly)
COPY ["Jellyfin.sln", "global.json", "nuget.config", "Directory.Build.props", "Directory.Packages.props", "./"]
COPY ["SharedVersion.cs", "BannedSymbols.txt", "stylecop.json", "./"]
# Copy all project files so dotnet restore can resolve the full dependency graph
COPY Emby.Naming/ Emby.Naming/
COPY Emby.Photos/ Emby.Photos/
COPY Emby.Server.Implementations/ Emby.Server.Implementations/
COPY Jellyfin.Api/ Jellyfin.Api/
COPY Jellyfin.Data/ Jellyfin.Data/
COPY Jellyfin.Server/ Jellyfin.Server/
COPY Jellyfin.Server.Implementations/ Jellyfin.Server.Implementations/
COPY MediaBrowser.Common/ MediaBrowser.Common/
COPY MediaBrowser.Controller/ MediaBrowser.Controller/
COPY MediaBrowser.LocalMetadata/ MediaBrowser.LocalMetadata/
COPY MediaBrowser.MediaEncoding/ MediaBrowser.MediaEncoding/
COPY MediaBrowser.Model/ MediaBrowser.Model/
COPY MediaBrowser.Providers/ MediaBrowser.Providers/
COPY MediaBrowser.XbmcMetadata/ MediaBrowser.XbmcMetadata/
COPY src/ src/
RUN dotnet restore Jellyfin.Server/Jellyfin.Server.csproj \
--runtime linux-x64
# Publish the server (and all transitive dependencies, including the
# PostgreSQL provider assembly added by this fork).
# Note: TreatWarningsAsErrors is disabled for the Docker build — StyleCop
# analyzer violations in upstream src/ projects would otherwise block the
# image build. StyleCop is enforced in the CI pipeline, not the Dockerfile.
RUN dotnet publish Jellyfin.Server/Jellyfin.Server.csproj \
--configuration Release \
--runtime linux-x64 \
--self-contained false \
--no-restore \
-p:TreatWarningsAsErrors=false \
--output /app
# ── Web client stage ──────────────────────────────────────────────────────────
# Install jellyfin-web via the official Jellyfin apt repo.
# Package suffix in the bookworm repo is +deb12 (e.g. 12.0+deb12).
# Web assets land at /usr/share/jellyfin/web/ — stable, prebuilt, no npm required.
# The web client version must match the server version this fork is based on.
FROM --platform=linux/amd64 debian:bookworm-slim AS webclient
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl gnupg ca-certificates \
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key \
| gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/jellyfin.gpg] https://repo.jellyfin.org/debian bookworm main" \
> /etc/apt/sources.list.d/jellyfin.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends "jellyfin-web=12.0+deb12" \
&& rm -rf /var/lib/apt/lists/* \
&& echo "Web client files:" && ls /usr/share/jellyfin/web/ | head -10
# ── Runtime stage ─────────────────────────────────────────────────────────────
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:10.0
# Install FFmpeg and native dependencies required by SkiaSharp and fontconfig.
# libicu, libssl, and liblttng-ust are already present in the dotnet/aspnet base image.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
fontconfig \
libfontconfig1 \
libfreetype6 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /jellyfin
COPY --from=build /app .
COPY --from=webclient /usr/share/jellyfin/web ./jellyfin-web/
# Jellyfin default ports
EXPOSE 8096
EXPOSE 8920
# Data / config volumes
VOLUME ["/config", "/cache", "/media"]
ENV JELLYFIN_DATA_DIR=/config \
JELLYFIN_CACHE_DIR=/cache \
JELLYFIN_LOG_DIR=/config/log \
JELLYFIN_CONFIG_DIR=/config
ENTRYPOINT ["./jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--webdir", "/jellyfin/jellyfin-web"]
+55
View File
@@ -0,0 +1,55 @@
# syntax=docker/dockerfile:1
# Runtime-only image — the .NET publish step runs on the CI host (runner),
# not inside this Dockerfile.
# ── Web client stage ──────────────────────────────────────────────────────────
# Install jellyfin-web via the official Jellyfin apt repo.
# Package suffix in the bookworm repo is +deb12 (e.g. 12.0+deb12).
# Web assets land at /usr/share/jellyfin/web/ — stable, prebuilt, no npm required.
# The web client version must match the server version this fork is based on.
FROM --platform=linux/amd64 debian:bookworm-slim AS webclient
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl gnupg ca-certificates \
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key \
| gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/jellyfin.gpg] https://repo.jellyfin.org/debian bookworm main" \
> /etc/apt/sources.list.d/jellyfin.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends "jellyfin-web=12.0+deb12" \
&& rm -rf /var/lib/apt/lists/*
# ── Runtime stage ─────────────────────────────────────────────────────────────
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:10.0
# Install FFmpeg and native dependencies required by SkiaSharp and fontconfig.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
fontconfig \
libfontconfig1 \
libfreetype6 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /jellyfin
# Copy the pre-built publish output produced by `dotnet publish` on the CI host.
COPY publish-output/ .
# Copy the jellyfin-web client assets from the webclient stage.
COPY --from=webclient /usr/share/jellyfin/web ./jellyfin-web/
# Jellyfin default ports
EXPOSE 8096
EXPOSE 8920
# Data / config volumes
VOLUME ["/config", "/cache", "/media"]
ENV JELLYFIN_DATA_DIR=/config \
JELLYFIN_CACHE_DIR=/cache \
JELLYFIN_LOG_DIR=/config/log
ENTRYPOINT ["./jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--webdir", "/jellyfin/jellyfin-web"]
+5 -3
View File
@@ -57,7 +57,6 @@ namespace Emby.Naming.Common
".nrg",
".nsv",
".nuv",
".ogg",
".ogm",
".ogv",
".pva",
@@ -152,7 +151,7 @@ namespace Emby.Naming.Common
CleanStrings =
[
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)",
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|web-dl|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)",
@"^\s*(?<cleaned>.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@@ -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
},
@@ -44,7 +44,14 @@ namespace Emby.Naming.ExternalFiles
}
var extension = Path.GetExtension(path.AsSpan());
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
// .idx carries VobSub per-track language metadata. Recognize it here rather
// than adding it to NamingOptions.SubtitleFileExtensions, which also gates
// subtitle uploads/saves.
var isVobSubIndex = _type == DlnaProfileType.Subtitle && extension.Equals(".idx", StringComparison.OrdinalIgnoreCase);
if (!isVobSubIndex
&& !(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Lyric && _namingOptions.LyricFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
{
+5 -3
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;
@@ -158,7 +158,9 @@ namespace Emby.Naming.TV
if (nextIndex >= name.Length
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
{
if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
// A range cannot end before it starts, so a lower number belongs to the episode title rather than to a range.
if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)
&& num >= result.EpisodeNumber)
{
result.EndingEpisodeNumber = num;
}
@@ -226,7 +228,7 @@ namespace Emby.Naming.TV
info.SeriesName = result.SeriesName;
}
if (!info.EndingEpisodeNumber.HasValue && info.EpisodeNumber.HasValue)
if (!info.EndingEpisodeNumber.HasValue && result.EndingEpisodeNumber >= info.EpisodeNumber)
{
info.EndingEpisodeNumber = result.EndingEpisodeNumber;
}
+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('/');
}
@@ -107,7 +107,8 @@ namespace Emby.Server.Implementations.Collections
SaveLocalMetadata = true
};
var name = _localizationManager.GetLocalizedString("Collections");
// This names a library for the whole server, so ignore the requesting client's language.
var name = _localizationManager.GetServerLocalizedString("Collections");
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
@@ -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");
+145 -41
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
@@ -179,6 +185,13 @@ namespace Emby.Server.Implementations.Dto
allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
}
// Batch-fetch by-name item counts to avoid N+1 queries
Dictionary<Guid, ItemCounts>? itemCountsBatch = null;
if (options.ContainsField(ItemFields.ItemCounts))
{
itemCountsBatch = GetItemCountsBatch(accessibleItems, user);
}
// Batch-fetch child counts for all folders to avoid N+1 queries
Dictionary<Guid, int>? childCountBatch = null;
if (options.ContainsField(ItemFields.ChildCount))
@@ -186,7 +199,7 @@ namespace Emby.Server.Implementations.Dto
var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
if (folderIds.Count > 0)
{
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user);
}
}
@@ -236,6 +249,29 @@ namespace Emby.Server.Implementations.Dto
artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
}
// Batch-fetch people across all items to avoid one GetPeople query per item.
IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null;
if (options.ContainsField(ItemFields.People))
{
var peopleItemIds = accessibleItems.Where(i => i.SupportsPeople).Select(i => i.Id).ToList();
if (peopleItemIds.Count > 0)
{
peopleBatch = _libraryManager.GetPeopleByItems(peopleItemIds);
}
}
// Batch-detect which videos own alternate versions to avoid the per-item alternate-version
// queries in MediaSourceCount. Videos absent from this set have a single media source.
IReadOnlySet<Guid>? alternateVersionItemIds = null;
if (options.ContainsField(ItemFields.MediaSourceCount))
{
var versionItemIds = accessibleItems.OfType<Video>().Select(i => i.Id).ToList();
if (versionItemIds.Count > 0)
{
alternateVersionItemIds = _libraryManager.GetItemIdsWithAlternateVersions(versionItemIds);
}
}
for (int index = 0; index < accessibleItems.Count; index++)
{
var item = accessibleItems[index];
@@ -248,7 +284,10 @@ namespace Emby.Server.Implementations.Dto
allCollectionFolders,
childCountBatch,
playedCountBatch,
artistsBatch);
artistsBatch,
resumeDataBatch?.GetValueOrDefault(item.Id),
peopleBatch,
alternateVersionItemIds);
if (item is LiveTvChannel tvChannel)
{
@@ -261,7 +300,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ItemCounts))
{
SetItemByNameInfo(dto, user);
SetItemByNameInfo(dto, user, itemCountsBatch);
}
returnItems[index] = dto;
@@ -309,7 +348,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 +365,15 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.People))
{
AttachPeople(dto, item, user);
IReadOnlyList<PersonInfo>? prefetchedPeople = null;
if (peopleBatch is not null)
{
// The batch omits items with no people, so a miss means "no people",
// not "not fetched". Use an empty list to skip the per-item query.
prefetchedPeople = peopleBatch.GetValueOrDefault(item.Id) ?? [];
}
AttachPeople(dto, item, user, prefetchedPeople);
}
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
@@ -353,7 +403,8 @@ namespace Emby.Server.Implementations.Dto
options,
userData,
childCountBatch,
playedCountBatch);
playedCountBatch,
resumeData);
}
if (item is IHasMediaSources
@@ -369,7 +420,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))
{
@@ -474,14 +525,36 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
private void SetItemByNameInfo(BaseItemDto dto, User? user)
private Dictionary<Guid, ItemCounts> GetItemCountsBatch(IReadOnlyList<BaseItem> items, User? user)
{
var result = new Dictionary<Guid, ItemCounts>();
foreach (var group in items.GroupBy(item => item.GetBaseItemKind()))
{
if (!_relatedItemKinds.TryGetValue(group.Key, out var relatedItemKinds))
{
continue;
}
var ids = group.Select(item => item.Id).ToArray();
foreach (var (id, counts) in _libraryManager.GetItemCountsForNameItems(group.Key, ids, relatedItemKinds, user))
{
result[id] = counts;
}
}
return result;
}
private void SetItemByNameInfo(BaseItemDto dto, User? user, IReadOnlyDictionary<Guid, ItemCounts>? prefetchedCounts = null)
{
if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
{
return;
}
var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
var counts = prefetchedCounts?.GetValueOrDefault(dto.Id)
?? _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
dto.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount;
@@ -538,7 +611,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)
{
@@ -566,7 +640,11 @@ namespace Emby.Server.Implementations.Dto
// For these types we can try to optimize and assume these values will be equal
if (item is MusicAlbum || item is Season || item is Playlist)
{
dto.ChildCount = dto.RecursiveItemCount;
if (dto.RecursiveItemCount > 0)
{
dto.ChildCount = dto.RecursiveItemCount;
}
var folderChildCount = folder.LinkedChildren.Length;
// The default is an empty array, so we can't reliably use the count when it's empty
if (folderChildCount > 0)
@@ -600,6 +678,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
{
@@ -648,7 +729,8 @@ namespace Emby.Server.Implementations.Dto
return count;
}
// Fall back to individual query for special cases (Series, Season, etc.)
// Only reached when no batch was computed: the batch holds an entry for every folder it
// was asked about, zero included.
return folder.GetChildCount(user);
}
@@ -729,12 +811,18 @@ namespace Emby.Server.Implementations.Dto
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <param name="user">The requesting user.</param>
private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null)
/// <param name="prefetchedPeople">People fetched in batch by the caller; when null the people are queried per item.</param>
private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null, IReadOnlyList<PersonInfo>? prefetchedPeople = null)
{
// When rendering a page of items the caller batch-fetches people for every item up
// front and passes them in, avoiding one GetPeople query per item. Fall back to the
// per-item query for the single item path where no batch is available.
var source = prefetchedPeople ?? _libraryManager.GetPeople(item);
// Ordering by person type to ensure actors and artists are at the front.
// This is taking advantage of the fact that they both begin with A
// This should be improved in the future
var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
var people = source.OrderBy(i => i.SortOrder ?? int.MaxValue)
.ThenBy(i =>
{
if (i.IsType(PersonKind.Actor))
@@ -943,7 +1031,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 +1164,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 +1347,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 +1473,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 +1607,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 +1628,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 +1686,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 +1745,7 @@ namespace Emby.Server.Implementations.Dto
break;
}
parent = GetImageDisplayParent(parent, item);
parent = GetImageDisplayParent(parent, item, artistsBatch);
}
}
@@ -65,6 +65,7 @@
<ItemGroup>
<PackageReference Include="Ignore" />
<PackageReference Include="StackExchange.Redis" />
</ItemGroup>
<ItemGroup>
@@ -27,6 +27,11 @@ namespace Emby.Server.Implementations.EntryPoints;
/// </summary>
public sealed class LibraryChangedNotifier : IHostedService, IDisposable
{
// A batch holds a live reference to every item it names, so it has to stay small enough that a
// library scan - which changes items faster than any batch window closes - cannot grow it without
// bound. Reached only by a scan; interactive use closes a batch on the window long before this.
internal const int MaxBatchSize = 2000;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IProviderManager _providerManager;
@@ -35,11 +40,11 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
private readonly ILogger<LibraryChangedNotifier> _logger;
private readonly Lock _libraryChangedSyncLock = new();
private readonly List<Folder> _foldersAddedTo = new();
private readonly List<Folder> _foldersRemovedFrom = new();
private readonly List<BaseItem> _itemsAdded = new();
private readonly List<BaseItem> _itemsRemoved = new();
private readonly List<BaseItem> _itemsUpdated = new();
private readonly Dictionary<Guid, Folder> _foldersAddedTo = [];
private readonly Dictionary<Guid, Folder> _foldersRemovedFrom = [];
private readonly Dictionary<Guid, BaseItem> _itemsAdded = [];
private readonly Dictionary<Guid, BaseItem> _itemsRemoved = [];
private readonly Dictionary<Guid, BaseItem> _itemsUpdated = [];
private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new();
private Timer? _libraryUpdateTimer;
@@ -173,7 +178,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
private void OnLibraryItemRemoved(object? sender, ItemChangeEventArgs e)
=> OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom);
private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder>? foldersList)
private void OnLibraryChange(BaseItem item, BaseItem parent, Dictionary<Guid, BaseItem> itemsList, Dictionary<Guid, Folder>? foldersList)
{
if (!FilterItem(item))
{
@@ -182,23 +187,28 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
lock (_libraryChangedSyncLock)
{
var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration);
// The window runs from the first change of a batch and is never extended. Extending it on
// every change would keep a library scan's batch open for the whole scan, and the batch
// holds the items it names alive, so it would grow to the size of the library.
if (_libraryUpdateTimer is null)
{
var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration);
_libraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan);
}
else
{
_libraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan);
}
if (foldersList is not null && parent is Folder folder)
{
foldersList.Add(folder);
foldersList[folder.Id] = folder;
}
itemsList.Add(item);
itemsList[item.Id] = item;
// A window long enough to cover a burst still has to give way once the batch is large
// enough to be worth sending on its own.
if (_itemsAdded.Count + _itemsRemoved.Count + _itemsUpdated.Count >= MaxBatchSize)
{
_libraryUpdateTimer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
}
}
}
@@ -211,22 +221,16 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
List<BaseItem> itemsRemoved;
lock (_libraryChangedSyncLock)
{
// Remove dupes in case some were saved multiple times
foldersAddedTo = _foldersAddedTo
.DistinctBy(x => x.Id)
.ToList();
foldersRemovedFrom = _foldersRemovedFrom
.DistinctBy(x => x.Id)
.ToList();
foldersAddedTo = _foldersAddedTo.Values.ToList();
foldersRemovedFrom = _foldersRemovedFrom.Values.ToList();
itemsUpdated = _itemsUpdated
.Where(i => !_itemsAdded.Contains(i))
.DistinctBy(x => x.Id)
.Where(e => !_itemsAdded.ContainsKey(e.Key))
.Select(e => e.Value)
.ToList();
itemsAdded = _itemsAdded.ToList();
itemsRemoved = _itemsRemoved.ToList();
itemsAdded = _itemsAdded.Values.ToList();
itemsRemoved = _itemsRemoved.Values.ToList();
if (_libraryUpdateTimer is not null)
{
@@ -241,6 +245,15 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
_foldersRemovedFrom.Clear();
}
if (itemsAdded.Count == 0
&& itemsUpdated.Count == 0
&& itemsRemoved.Count == 0
&& foldersAddedTo.Count == 0
&& foldersRemovedFrom.Count == 0)
{
return;
}
await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
}
@@ -18,15 +18,17 @@ namespace Emby.Server.Implementations.EntryPoints
public sealed class UserDataChangeNotifier : IHostedService, IDisposable
{
private const int UpdateDuration = 500;
internal const int MaxBatchSize = 2000;
private readonly ISessionManager _sessionManager;
private readonly IUserDataManager _userDataManager;
private readonly IUserManager _userManager;
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
private readonly Dictionary<Guid, Dictionary<Guid, BaseItem>> _changedItems = [];
private readonly Lock _syncLock = new();
private Timer? _updateTimer;
private int _changedItemCount;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataChangeNotifier"/> class.
@@ -69,50 +71,64 @@ namespace Emby.Server.Implementations.EntryPoints
lock (_syncLock)
{
if (_updateTimer is null)
{
_updateTimer = new Timer(
UpdateTimerCallback,
null,
UpdateDuration,
Timeout.Infinite);
}
else
{
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
}
// The window runs from the first change of a batch and is never extended, so a stream
// of changes that never pauses - a library scan - still closes its batches instead of
// holding every item it touched alive until the stream stops.
_updateTimer ??= new Timer(
UpdateTimerCallback,
null,
UpdateDuration,
Timeout.Infinite);
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys))
if (!_changedItems.TryGetValue(e.UserId, out Dictionary<Guid, BaseItem>? keys))
{
keys = new List<BaseItem>();
keys = [];
_changedItems[e.UserId] = keys;
}
keys.Add(e.Item);
var baseItem = e.Item;
// Go up one level for indicators
if (baseItem is not null)
{
Track(keys, baseItem);
var parent = baseItem.GetOwner() ?? baseItem.GetParent();
if (parent is not null)
{
keys.Add(parent);
Track(keys, parent);
}
}
// A window long enough to cover a burst still has to give way once the batch is
// large enough to be worth sending on its own.
if (_changedItemCount >= MaxBatchSize)
{
_updateTimer.Change(0, Timeout.Infinite);
}
}
}
private void Track(Dictionary<Guid, BaseItem> keys, BaseItem item)
{
var before = keys.Count;
keys[item.Id] = item;
if (keys.Count != before)
{
_changedItemCount++;
}
}
private async void UpdateTimerCallback(object? state)
{
List<KeyValuePair<Guid, List<BaseItem>>> changes;
List<KeyValuePair<Guid, Dictionary<Guid, BaseItem>>> changes;
lock (_syncLock)
{
// Remove dupes in case some were saved multiple times
changes = _changedItems.ToList();
_changedItems.Clear();
_changedItemCount = 0;
if (_updateTimer is not null)
{
@@ -121,17 +137,22 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
if (changes.Count == 0)
{
return;
}
foreach (var (userId, changedItems) in changes)
{
await _sessionManager.SendMessageToUserSessions(
[userId],
SessionMessageType.UserDataChanged,
() => GetUserDataChangeInfo(userId, changedItems),
() => GetUserDataChangeInfo(userId, changedItems.Values),
default).ConfigureAwait(false);
}
}
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, IEnumerable<BaseItem> changedItems)
{
var user = _userManager.GetUserById(userId)
?? throw new ArgumentException("Invalid user ID", nameof(userId));
@@ -140,7 +161,6 @@ namespace Emby.Server.Implementations.EntryPoints
{
UserId = userId,
UserDataList = changedItems
.DistinctBy(x => x.Id)
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
@@ -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;
}
@@ -8,6 +8,7 @@ using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -21,6 +22,7 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly IDirectoryService _directoryService;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
/// <summary>
@@ -47,6 +49,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="libraryManager">The library manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="directoryService">The directory service.</param>
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
public LibraryMonitor(
@@ -54,6 +57,7 @@ namespace Emby.Server.Implementations.IO
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
IDirectoryService directoryService,
IHostApplicationLifetime appLifetime,
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
{
@@ -61,6 +65,7 @@ namespace Emby.Server.Implementations.IO
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
_directoryService = directoryService;
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
appLifetime.ApplicationStarted.Register(Start);
@@ -363,6 +368,8 @@ namespace Emby.Server.Implementations.IO
return;
}
_directoryService.Invalidate(path);
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
foreach (var i in _tempIgnoredPaths.Keys)
{
@@ -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
});
}
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -15,7 +16,6 @@ using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks.Tasks;
using Emby.Server.Implementations.Sorting;
@@ -35,7 +35,6 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
@@ -45,6 +44,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;
@@ -74,7 +74,6 @@ namespace Emby.Server.Implementations.Library
private readonly Lazy<IProviderManager> _providerManagerFactory;
private readonly Lazy<IUserViewManager> _userViewManagerFactory;
private readonly IServerApplicationHost _appHost;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
private readonly IItemPersistenceService _persistenceService;
@@ -86,6 +85,8 @@ namespace Emby.Server.Implementations.Library
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager;
private readonly ILocalizationManager _localization;
private readonly IDirectoryService _directoryService;
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
private readonly IMediaStreamRepository _mediaStreamRepository;
@@ -120,7 +121,6 @@ namespace Emby.Server.Implementations.Library
/// <param name="fileSystem">The file system.</param>
/// <param name="providerManagerFactory">The provider manager.</param>
/// <param name="userViewManagerFactory">The user view manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="persistenceService">The item persistence service.</param>
/// <param name="nextUpService">The next up service.</param>
@@ -132,6 +132,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(
@@ -145,7 +146,6 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem,
Lazy<IProviderManager> providerManagerFactory,
Lazy<IUserViewManager> userViewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IItemPersistenceService persistenceService,
INextUpService nextUpService,
@@ -157,6 +157,7 @@ namespace Emby.Server.Implementations.Library
IPeopleRepository peopleRepository,
IPathManager pathManager,
DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
ILocalizationManager localization,
IMediaStreamRepository mediaStreamRepository,
Lazy<IExternalDataManager> externalDataManagerFactory)
{
@@ -170,7 +171,6 @@ namespace Emby.Server.Implementations.Library
_fileSystem = fileSystem;
_providerManagerFactory = providerManagerFactory;
_userViewManagerFactory = userViewManagerFactory;
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_persistenceService = persistenceService;
_nextUpService = nextUpService;
@@ -184,6 +184,8 @@ namespace Emby.Server.Implementations.Library
_peopleRepository = peopleRepository;
_pathManager = pathManager;
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
_localization = localization;
_directoryService = directoryService;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -407,6 +409,13 @@ namespace Emby.Server.Implementations.Library
}
_persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
// Evict the deleted items from the cache and announce each removal.
foreach (var (item, _, _) in pathMaps)
{
_cache.TryRemove(item.Id, out _);
ReportItemRemoved(item, item.GetOwner() ?? item.GetParent());
}
}
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
@@ -606,6 +615,12 @@ namespace Emby.Server.Implementations.Library
folder.UserData = null;
}
// Announce the descendants before the item itself.
foreach (var child in children)
{
ReportItemRemoved(child, item);
}
ReportItemRemoved(item, parent);
}
@@ -1191,6 +1206,12 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault();
}
/// <inheritdoc />
public Guid GetPersonId(string name)
{
return GetItemByNameId<Person>(Person.GetPath(name));
}
/// <inheritdoc />
public Person? GetPerson(string name)
{
@@ -1204,6 +1225,33 @@ namespace Emby.Server.Implementations.Library
return null;
}
/// <inheritdoc />
public Person GetOrCreatePerson(string name)
{
var existing = GetPerson(name);
if (existing is not null)
{
return existing;
}
var path = Person.GetPath(name);
var info = Directory.CreateDirectory(path);
var item = new Person
{
Name = name,
Id = GetItemByNameId<Person>(path),
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
Path = path
};
item.PresentationUniqueKey = item.CreatePresentationUniqueKey();
CreateItem(item, null);
return item;
}
/// <summary>
/// Gets the studio.
/// </summary>
@@ -1336,15 +1384,6 @@ namespace Emby.Server.Implementations.Library
return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
}
/// <inheritdoc />
public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
// Ensure the location is available.
Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
}
/// <summary>
/// Reloads the root media folder.
/// </summary>
@@ -1471,6 +1510,10 @@ namespace Emby.Server.Implementations.Library
var numComplete = 0;
var numTasks = tasks.Count;
_logger.LogInformation("Running {TaskCount} post-scan task(s)", numTasks);
var phaseStart = Stopwatch.GetTimestamp();
foreach (var task in tasks)
{
// Prevent access to modified closure
@@ -1488,20 +1531,45 @@ namespace Emby.Server.Implementations.Library
progress.Report(innerPercent);
});
_logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
var taskName = task.GetType().Name;
var taskStart = Stopwatch.GetTimestamp();
_logger.LogInformation(
"Running post-scan task {TaskNumber}/{TaskCount}: {TaskName}",
currentNumComplete + 1,
numTasks,
taskName);
try
{
await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
var elapsed = Stopwatch.GetElapsedTime(taskStart);
_logger.LogInformation(
"Post-scan task {TaskName} completed after {Minutes} minute(s) and {Seconds} seconds",
taskName,
Math.Truncate(elapsed.TotalMinutes),
elapsed.Seconds);
}
catch (OperationCanceledException)
{
_logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
var elapsed = Stopwatch.GetElapsedTime(taskStart);
_logger.LogInformation(
"Post-scan task {TaskName} cancelled after {Minutes} minute(s) and {Seconds} seconds",
taskName,
Math.Truncate(elapsed.TotalMinutes),
elapsed.Seconds);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error running post-scan task");
var elapsed = Stopwatch.GetElapsedTime(taskStart);
_logger.LogError(
ex,
"Post-scan task {TaskName} failed after {Minutes} minute(s) and {Seconds} seconds",
taskName,
Math.Truncate(elapsed.TotalMinutes),
elapsed.Seconds);
}
numComplete++;
@@ -1510,6 +1578,12 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100);
}
var phaseElapsed = Stopwatch.GetElapsedTime(phaseStart);
_logger.LogInformation(
"All post-scan tasks completed after {Minutes} minute(s) and {Seconds} seconds",
Math.Truncate(phaseElapsed.TotalMinutes),
phaseElapsed.Seconds);
_persistenceService.UpdateInheritedValues();
progress.Report(100);
@@ -1727,9 +1801,21 @@ namespace Emby.Server.Implementations.Library
return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
}
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
/// <inheritdoc/>
public Dictionary<Guid, ItemCounts> GetItemCountsForNameItems(BaseItemKind kind, IReadOnlyList<Guid> ids, BaseItemKind[] relatedItemKinds, User? user)
{
return _countService.GetChildCountBatch(parentIds, userId);
var query = new InternalItemsQuery(user);
if (user is not null)
{
AddUserToQuery(query, user);
}
return _countService.GetItemCountsForNameItems(kind, ids, relatedItemKinds, query);
}
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user)
{
return _countService.GetChildCountBatch(parentIds, user);
}
/// <inheritdoc/>
@@ -1896,14 +1982,14 @@ namespace Emby.Server.Implementations.Library
}
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
query.AncestorIds = [];
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
var topParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
if (topParentIds.Length == 0)
{
query.TopParentIds = [Guid.NewGuid()];
return;
}
query.TopParentIds = topParentIds;
query.AncestorIds = [];
}
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
@@ -1949,12 +2035,15 @@ namespace Emby.Server.Implementations.Library
if (parents.All(i => i is ICollectionFolder || i is UserView))
{
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
var topParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
if (topParentIds.Length > 0)
{
query.TopParentIds = [Guid.NewGuid()];
query.TopParentIds = topParentIds;
}
else
{
SetAncestorIds(query, parents);
}
}
else if (parents.Count == 1 && parents.First() is Folder folder
@@ -1963,34 +2052,31 @@ namespace Emby.Server.Implementations.Library
{
// Playlists and BoxSets store their contents in LinkedChildren and never
// populate AncestorIds for those items, so a recursive AncestorIds query
// would return zero rows. Resolve to the linked child IDs up front and
// route through the existing indexed ItemIds filter.
query.ItemIds = folder.LinkedChildren
.Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
.Select(lc => lc.ItemId!.Value)
.ToArray();
// Empty linked-children should still return empty rather than scanning everything.
if (query.ItemIds.Length == 0)
{
query.ItemIds = [Guid.NewGuid()];
}
// would return zero rows. Filter by the descendant set instead, which follows
// the links and keeps descending, so a linked folder contributes what is below
// it as well - the episodes of a Series added to a collection, for example.
query.DescendantOfId = folder.Id;
}
else
{
// We need to be able to query from any arbitrary ancestor up the tree
query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
// Prevent searching in all libraries due to empty filter
if (query.AncestorIds.Length == 0)
{
query.AncestorIds = [Guid.NewGuid()];
}
SetAncestorIds(query, parents);
}
query.Parent = null;
}
private static void SetAncestorIds(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
{
// We need to be able to query from any arbitrary ancestor up the tree
query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
// Prevent searching in all libraries due to empty filter
if (query.AncestorIds.Length == 0)
{
query.AncestorIds = [Guid.NewGuid()];
}
}
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
{
if (query.User is null)
@@ -2229,6 +2315,12 @@ namespace Emby.Server.Implementations.Library
return [];
}
/// <inheritdoc />
public IReadOnlySet<Guid> GetItemIdsWithAlternateVersions(IReadOnlyList<Guid> itemIds)
{
return _linkedChildrenService.GetItemIdsWithAlternateVersions(itemIds);
}
/// <inheritdoc />
public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType childType)
{
@@ -2315,9 +2407,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 +2463,7 @@ namespace Emby.Server.Implementations.Library
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
altVideo.IsInMixedFolder = video.IsInMixedFolder;
// ResolveAlternateVersion only sees the alternate's primary file.
// If the alternate is itself a stack (e.g. 1080p part1 + part2),
// detect its parts from sibling files so its AdditionalParts persist.
@@ -2490,9 +2587,15 @@ namespace Emby.Server.Implementations.Library
}
}
if (!File.Exists(image.Path))
if (string.IsNullOrEmpty(image.Path) || !File.Exists(image.Path))
{
_logger.LogWarning("Image not found at {ImagePath}", image.Path);
_logger.LogWarning(
"{ImageType} image for {ItemName} ({ItemId}) not found at \"{ImagePath}\", source was {SourcePath}",
img.Type,
item.Name,
item.Id,
image.Path,
img.Path);
continue;
}
@@ -2552,6 +2655,8 @@ namespace Emby.Server.Implementations.Library
item.DateLastSaved = DateTime.UtcNow;
}
ForgetDroppedLocalAlternateVersions(items);
// Resolve and add any local alternate version items that don't exist yet
// This ensures they exist in the database when LinkedChildren are processed
var allItems = new List<BaseItem>(items);
@@ -2580,6 +2685,7 @@ namespace Emby.Server.Implementations.Library
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
altVideo.IsInMixedFolder = video.IsInMixedFolder;
// ResolveAlternateVersion only sees the alternate's primary file.
// If the alternate is itself a stack (e.g. 1080p part1 + part2),
// detect its parts from sibling files so its AdditionalParts persist.
@@ -2640,6 +2746,30 @@ namespace Emby.Server.Implementations.Library
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
/// <summary>
/// Forgets the cached local alternate versions of the supplied items that they no longer list.
/// </summary>
/// <param name="items">The items about to be saved.</param>
private void ForgetDroppedLocalAlternateVersions(IReadOnlyList<BaseItem> items)
{
foreach (var video in items.OfType<Video>())
{
var videoType = video.GetType();
var keptIds = video.LocalAlternateVersions
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => GetNewItemId(path, videoType))
.ToHashSet();
foreach (var versionId in GetLocalAlternateVersionIds(video))
{
if (!keptIds.Contains(versionId))
{
_cache.TryRemove(versionId, out _);
}
}
}
}
/// <inheritdoc />
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
{
@@ -2863,7 +2993,8 @@ namespace Emby.Server.Implementations.Library
"views",
_fileSystem.GetValidFilename(viewType.ToString()));
var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
// The display name is localized, so it must not take part in the id.
var id = GetNewItemId(path + "_namedview_" + viewType.ToString(), typeof(UserView));
var item = GetItemById(id) as UserView;
@@ -2887,6 +3018,13 @@ namespace Emby.Server.Implementations.Library
refresh = true;
}
else if (!string.Equals(item.Name, name, StringComparison.Ordinal))
{
item.Name = name;
item.ForcedSortName = sortName;
refresh = true;
}
if (refresh)
{
@@ -2907,7 +3045,9 @@ namespace Emby.Server.Implementations.Library
var parentIdString = parentId.IsEmpty()
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
// The name is either localized (grouped views) or the library folder's own name.
var idValues = "38_namedview_" + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
var id = GetNewItemId(idValues, typeof(UserView));
@@ -2937,6 +3077,11 @@ namespace Emby.Server.Implementations.Library
isNew = true;
}
else if (!string.Equals(item.Name, name, StringComparison.Ordinal))
{
item.Name = name;
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
var lastRefreshedUtc = item.DateLastRefreshed;
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
@@ -3038,7 +3183,7 @@ namespace Emby.Server.Implementations.Library
var parentIdString = parentId.IsEmpty()
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
var idValues = "37_namedview_" + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
if (!string.IsNullOrEmpty(uniqueId))
{
idValues += uniqueId;
@@ -3072,9 +3217,10 @@ namespace Emby.Server.Implementations.Library
isNew = true;
}
if (viewType != item.ViewType)
if (viewType != item.ViewType || !string.Equals(item.Name, name, StringComparison.Ordinal))
{
item.ViewType = viewType;
item.Name = name;
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
@@ -3195,11 +3341,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 +3422,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 +3440,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 +3492,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 +3511,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 +3519,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)
@@ -3418,12 +3640,24 @@ namespace Emby.Server.Implementations.Library
return _peopleRepository.GetPeopleNames(query);
}
/// <inheritdoc/>
public int DeleteOrphanedCredits()
{
return _peopleRepository.DeleteOrphanedCredits();
}
/// <inheritdoc/>
public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes)
{
return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes);
}
/// <inheritdoc/>
public IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds)
{
return _peopleRepository.GetPeopleByItems(itemIds);
}
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
{
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
@@ -3457,7 +3691,20 @@ namespace Emby.Server.Implementations.Library
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return item.GetImageInfo(image.Type, imageIndex);
var localImage = item.GetImageInfo(image.Type, imageIndex);
if (localImage is null)
{
throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture,
"Downloaded {0} image {1} from {2} is not attached to {3} ({4})",
image.Type,
imageIndex,
url,
item.Name,
item.Id));
}
return localImage;
}
catch (HttpRequestException ex)
{
@@ -3479,7 +3726,13 @@ namespace Emby.Server.Implementations.Library
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
}
throw new InvalidOperationException("Unable to convert any images to local");
throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture,
"Unable to convert any {0} image url in \"{1}\" to a local file for {2} ({3})",
image.Type,
image.Path,
item.Name,
item.Id));
}
public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
@@ -3535,6 +3788,10 @@ namespace Emby.Server.Implementations.Library
AddMediaPathInternal(name, path, false);
}
}
// The libraries root was listed before this folder existed, so drop that listing:
// anything still reading it resolves the library set without the new folder.
_directoryService.Invalidate(virtualFolderPath);
}
finally
{
@@ -3561,27 +3818,14 @@ namespace Emby.Server.Implementations.Library
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
var createEntity = false;
var personEntity = GetPerson(person.Name);
if (personEntity is null)
{
try
{
var path = Person.GetPath(person.Name);
var info = Directory.CreateDirectory(path);
personEntity = new Person()
{
Name = person.Name,
Id = GetItemByNameId<Person>(path),
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
Path = path
};
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
personEntity = GetOrCreatePerson(person.Name);
saveEntity = true;
createEntity = true;
}
catch (Exception ex)
{
@@ -3615,11 +3859,6 @@ namespace Emby.Server.Implementations.Library
if (saveEntity)
{
if (createEntity)
{
CreateItems([personEntity], null, CancellationToken.None);
}
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
personEntity.DateLastSaved = DateTime.UtcNow;
@@ -3659,7 +3898,9 @@ namespace Emby.Server.Implementations.Library
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName)
?? throw new FileNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
CreateShortcut(virtualFolderPath, pathInfo);
@@ -3680,7 +3921,9 @@ namespace Emby.Server.Implementations.Library
ArgumentNullException.ThrowIfNull(mediaPath);
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName)
?? throw new FileNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
@@ -3719,9 +3962,9 @@ namespace Emby.Server.Implementations.Library
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var path = Path.Combine(rootFolderPath, name);
var path = FileSystemHelper.GetChildPath(rootFolderPath, name);
if (!Directory.Exists(path))
if (path is null || !Directory.Exists(path))
{
throw new FileNotFoundException("The media folder does not exist");
}
@@ -3731,6 +3974,7 @@ namespace Emby.Server.Implementations.Library
try
{
Directory.Delete(path, true);
_directoryService.Invalidate(path);
}
finally
{
@@ -3785,9 +4029,9 @@ namespace Emby.Server.Implementations.Library
ArgumentException.ThrowIfNullOrEmpty(mediaPath);
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName);
if (!Directory.Exists(virtualFolderPath))
if (virtualFolderPath is null || !Directory.Exists(virtualFolderPath))
{
throw new FileNotFoundException(
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
@@ -3800,6 +4044,7 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(shortcut))
{
_fileSystem.DeleteFile(shortcut);
_directoryService.Invalidate(shortcut);
}
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
@@ -3843,6 +4088,7 @@ namespace Emby.Server.Implementations.Library
}
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
_directoryService.Invalidate(lnk);
RemoveContentTypeOverrides(path);
}
@@ -3886,5 +4132,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
@@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
args.LibraryOptions.SeasonZeroDisplayName :
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameSeasonNumber"),
_localization.GetServerLocalizedString("NameSeasonNumber"),
seasonNumber,
args.LibraryOptions.PreferredMetadataLanguage);
}
@@ -129,6 +129,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var tmdbId = justName.GetAttributeValue("tmdbid");
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
// Anime databases model a single cour as its own entry, so a multi-season
// series maps to one of these ids per season rather than one per series.
var anidbId = justName.GetAttributeValue("anidbid");
item.TrySetProviderId("AniDB", anidbId);
var aniListId = justName.GetAttributeValue("anilistid");
item.TrySetProviderId("AniList", aniListId);
var aniSearchId = justName.GetAttributeValue("anisearchid");
item.TrySetProviderId("AniSearch", aniSearchId);
}
}
}
@@ -92,49 +92,45 @@ public class SearchManager : ISearchManager
await Task.WhenAll(externalTask, internalTask).ConfigureAwait(false);
var externalResults = await externalTask.ConfigureAwait(false);
var fromExternal = externalResults.Count > 0;
IReadOnlyList<SearchResult> results;
if (fromExternal)
{
results = externalResults;
}
else
{
results = await internalTask.ConfigureAwait(false);
if (_internalProviders.Length > 0)
{
_logger.LogDebug("No results from external providers, using internal provider results");
}
}
// Internal providers apply user-access filtering inline in their queries. External
// providers don't know about user permissions, so they may return IDs from hidden
// libraries or items the user is otherwise blocked from. Run the post-filter only
// when results came from externals to close that gap. The Items controller's second
// roundtrip via folder.GetItems applies most of these again, but it does not restrict
// by TopParentIds when ItemIds is set.
if (fromExternal && results.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
// libraries or items the user is otherwise blocked from. Filter them here to close
// that gap. The Items controller's second roundtrip via folder.GetItems applies most
// of these again, but it does not restrict by TopParentIds when ItemIds is set.
if (externalResults.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
{
var user = _userManager.GetUserById(query.UserId.Value);
if (user is not null)
{
results = await FilterByUserAccessAsync(results, user, cancellationToken).ConfigureAwait(false);
externalResults = await FilterByUserAccessAsync(externalResults, user, query, cancellationToken).ConfigureAwait(false);
}
}
return results;
if (externalResults.Count > 0)
{
return externalResults;
}
if (_internalProviders.Length > 0)
{
_logger.LogDebug("No results from external providers, using internal provider results");
}
return await internalTask.ConfigureAwait(false);
}
private async Task<IReadOnlyList<SearchResult>> FilterByUserAccessAsync(
IReadOnlyList<SearchResult> candidates,
User user,
SearchProviderQuery query,
CancellationToken cancellationToken)
{
// SetUser populates parental rating + blocked/allowed tags. ConfigureUserAccess populates
// TopParentIds for the user's accessible libraries — we call it before assigning ItemIds
// because LibraryManager.AddUserToQuery skips TopParentIds when ItemIds is non-empty.
var accessFilter = new InternalItemsQuery(user);
_libraryManager.ConfigureUserAccess(accessFilter, user);
// SetUser populates parental rating + blocked/allowed tags, Build populates TopParentIds
// for the user's accessible libraries. The candidate ids are applied to the query below
// rather than to the filter because LibraryManager.AddUserToQuery skips TopParentIds when
// ItemIds is non-empty.
var accessFilter = SearchQueryAccessFilter.Build(user, query, _libraryManager);
Guid[] candidateIds = [.. candidates.Select(c => c.ItemId)];
@@ -147,17 +143,16 @@ public class SearchManager : ISearchManager
baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, accessFilter);
var allowedCount = await baseQuery.CountAsync(cancellationToken).ConfigureAwait(false);
if (allowedCount == candidates.Count)
{
return candidates;
}
var allowedIds = await baseQuery
.Select(e => e.Id)
.ToHashSetAsync(cancellationToken)
.ConfigureAwait(false);
if (allowedIds.Count == candidates.Count)
{
return candidates;
}
var filtered = candidates.Where(c => allowedIds.Contains(c.ItemId)).ToList();
if (filtered.Count < candidates.Count)
{
@@ -0,0 +1,38 @@
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
namespace Emby.Server.Implementations.Library.Search;
/// <summary>
/// Builds the access filter that decides which items a search may return for a user.
/// </summary>
internal static class SearchQueryAccessFilter
{
/// <summary>
/// Builds an access filter carrying the search's library access and type filters.
/// </summary>
/// <param name="user">The user the search runs for.</param>
/// <param name="query">The search query.</param>
/// <param name="libraryManager">The library manager.</param>
/// <returns>The access filter.</returns>
public static InternalItemsQuery Build(User user, SearchProviderQuery query, ILibraryManager libraryManager)
{
// The type filters have to travel with the access filter: a by-name item belongs to no
// library, so it carries no TopParentId to match, and the library filter only knows to
// exempt it when the query says those types are wanted. A search scoped to a parent gets
// no exemption because a by-name item has no parent to descend from either.
var accessFilter = new InternalItemsQuery(user)
{
IncludeItemTypes = query.IncludeItemTypes,
ExcludeItemTypes = query.ExcludeItemTypes,
IncludeItemsByName = !query.ParentId.HasValue || query.ParentId.Value.IsEmpty()
};
// ConfigureUserAccess populates TopParentIds for the libraries the user may open.
libraryManager.ConfigureUserAccess(accessFilter, user);
return accessFilter;
}
}
@@ -114,7 +114,7 @@ public class SqlSearchProvider : IInternalSearchProvider
dbQuery = ApplyTypeFilter(dbQuery, query.IncludeItemTypes, query.ExcludeItemTypes);
dbQuery = ApplyMediaTypeFilter(dbQuery, query.MediaTypes);
dbQuery = ApplyParentFilter(dbQuery, query.ParentId);
dbQuery = ApplyUserAccessFilter(dbContext, dbQuery, query.UserId);
dbQuery = ApplyUserAccessFilter(dbContext, dbQuery, query);
// Compute the score in SQL: the ternary translates to a CASE WHEN. CleanName is
// the pre-normalized (lowercase, diacritic-stripped) form, so we score against it
@@ -196,8 +196,9 @@ public class SqlSearchProvider : IInternalSearchProvider
private IQueryable<BaseItemEntity> ApplyUserAccessFilter(
JellyfinDbContext dbContext,
IQueryable<BaseItemEntity> query,
Guid? userId)
SearchProviderQuery searchQuery)
{
var userId = searchQuery.UserId;
if (!userId.HasValue || userId.Value.IsEmpty())
{
return query;
@@ -209,8 +210,7 @@ public class SqlSearchProvider : IInternalSearchProvider
return query;
}
var accessFilter = new InternalItemsQuery(user);
_libraryManager.ConfigureUserAccess(accessFilter, user);
var accessFilter = SearchQueryAccessFilter.Build(user, searchQuery, _libraryManager);
return _queryHelpers.ApplyAccessFiltering(dbContext, query, accessFilter);
}
@@ -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);
@@ -251,7 +260,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
}
var candidateRows = await context.ItemValuesMap.AsNoTracking()
.Where(m => m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue))
.Where(m => !m.Item.PrimaryVersionId.HasValue && m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue))
.Select(m => new { m.ItemId, Key = m.ItemValue.CleanValue })
.ToListAsync(cancellationToken).ConfigureAwait(false);
@@ -267,6 +276,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
if (personSourceRows.Count > 0)
{
var personCandidateRows = await context.PeopleBaseItemMap.AsNoTracking()
.Where(m => !m.Item.PrimaryVersionId.HasValue)
.Where(m => context.PeopleBaseItemMap
.Where(s => sourceIds.Contains(s.ItemId) && _scoredPersonTypes.Contains(s.People.PersonType))
.Select(s => s.PeopleId)
@@ -0,0 +1,42 @@
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Builds the access filter that decides which items a similar-items lookup may return for a user.
/// </summary>
internal static class SimilarItemsAccessFilter
{
private static readonly BaseItemKind[] _itemByNameKinds =
[
BaseItemKind.Person,
BaseItemKind.Genre,
BaseItemKind.MusicGenre,
BaseItemKind.MusicArtist,
BaseItemKind.Studio
];
/// <summary>
/// Builds an access filter carrying the user's library access and parental restrictions.
/// </summary>
/// <param name="user">The user the lookup runs for.</param>
/// <param name="libraryManager">The library manager.</param>
/// <returns>The access filter.</returns>
public static InternalItemsQuery Build(User user, ILibraryManager libraryManager)
{
// IncludeItemTypes is read only for the by-name exemption here; the caller applies this
// filter through ApplyAccessFiltering, which does not translate it into a type restriction.
var accessFilter = new InternalItemsQuery(user)
{
IncludeItemTypes = _itemByNameKinds
};
// ConfigureUserAccess populates TopParentIds for the libraries the user may open.
libraryManager.ConfigureUserAccess(accessFilter, user);
return accessFilter;
}
}
@@ -7,8 +7,10 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -16,11 +18,13 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.SimilarItems;
@@ -35,6 +39,8 @@ public class SimilarItemsManager : ISimilarItemsManager
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IItemQueryHelpers _queryHelpers;
private ISimilarItemsProvider[] _similarItemsProviders = [];
/// <summary>
@@ -45,18 +51,24 @@ public class SimilarItemsManager : ISimilarItemsManager
/// <param name="libraryManager">The library manager.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
/// <param name="dbProvider">The database context factory.</param>
/// <param name="queryHelpers">The shared item query helpers.</param>
public SimilarItemsManager(
ILogger<SimilarItemsManager> logger,
IServerApplicationPaths appPaths,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager serverConfigurationManager)
IServerConfigurationManager serverConfigurationManager,
IDbContextFactory<JellyfinDbContext> dbProvider,
IItemQueryHelpers queryHelpers)
{
_logger = logger;
_appPaths = appPaths;
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_serverConfigurationManager = serverConfigurationManager;
_dbProvider = dbProvider;
_queryHelpers = queryHelpers;
}
/// <inheritdoc/>
@@ -183,6 +195,7 @@ public class SimilarItemsManager : ISimilarItemsManager
// Collect references in batches and resolve against local library.
// Stop fetching once we have enough resolved local items.
const int BatchSize = 20;
const int MaxRemoteReferenceFetchLimit = 500;
var remaining = requestedLimit - allResults.Count;
var collectedReferences = new List<SimilarItemReference>();
var pendingBatch = new List<SimilarItemReference>();
@@ -199,7 +212,7 @@ public class SimilarItemsManager : ISimilarItemsManager
remaining -= resolvedItems.Count;
pendingBatch.Clear();
if (remaining <= 0)
if (remaining <= 0 || collectedReferences.Count >= MaxRemoteReferenceFetchLimit)
{
break;
}
@@ -229,11 +242,64 @@ public class SimilarItemsManager : ISimilarItemsManager
}
}
return allResults
var ordered = allResults
.OrderByDescending(x => x.Score)
.Select(x => x.Item)
.Take(requestedLimit)
.ToList();
return await FilterByLibraryAccessAsync(ordered, user, cancellationToken).ConfigureAwait(false);
}
private async Task<IReadOnlyList<BaseItem>> FilterByLibraryAccessAsync(
IReadOnlyList<BaseItem> candidates,
User? user,
CancellationToken cancellationToken)
{
if (candidates.Count == 0 || user is null)
{
return candidates;
}
var accessFilter = SimilarItemsAccessFilter.Build(user, _libraryManager);
// No accessible libraries means nothing to compare against, and an empty TopParentIds set
// would disable the filter rather than reject everything.
if (accessFilter.TopParentIds.Length == 0)
{
return candidates;
}
Guid[] candidateIds = [.. candidates.Select(c => c.Id)];
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var baseQuery = dbContext.BaseItems
.AsNoTracking()
.WhereOneOrMany(candidateIds, e => e.Id);
baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, accessFilter);
var allowedCount = await baseQuery.CountAsync(cancellationToken).ConfigureAwait(false);
if (allowedCount == candidates.Count)
{
return candidates;
}
var allowedIds = await baseQuery
.Select(e => e.Id)
.ToHashSetAsync(cancellationToken)
.ConfigureAwait(false);
var filtered = candidates.Where(c => allowedIds.Contains(c.Id)).ToList();
_logger.LogDebug(
"Dropped {Dropped} of {Total} similar-item candidates due to user access filtering",
candidates.Count - filtered.Count,
candidates.Count);
return filtered;
}
}
/// <inheritdoc/>
@@ -375,19 +441,39 @@ public class SimilarItemsManager : ISimilarItemsManager
var batchResults = await batchProvider.GetBatchSimilarItemsAsync(baselineItems, query, cancellationToken).ConfigureAwait(false);
// Filter once across every category rather than per baseline, so a batch provider costs one
// access query no matter how many categories it produced.
var allItems = batchResults.Values.SelectMany(items => items).DistinctBy(item => item.Id).ToList();
var allowed = await FilterByLibraryAccessAsync(allItems, query.User, cancellationToken).ConfigureAwait(false);
HashSet<Guid>? allowedIds = allowed.Count == allItems.Count
? null
: [.. allowed.Select(item => item.Id)];
var recommendations = new List<SimilarItemsRecommendation>(baselineItems.Count);
foreach (var baseline in baselineItems)
{
if (batchResults.TryGetValue(baseline.Id, out var similar) && similar.Count > 0)
if (!batchResults.TryGetValue(baseline.Id, out var similar) || similar.Count == 0)
{
recommendations.Add(new SimilarItemsRecommendation
{
BaselineItemName = baseline.Name,
CategoryId = baseline.Id,
RecommendationType = recommendationType,
Items = similar
});
continue;
}
if (allowedIds is not null)
{
similar = similar.Where(item => allowedIds.Contains(item.Id)).ToList();
if (similar.Count == 0)
{
continue;
}
}
recommendations.Add(new SimilarItemsRecommendation
{
BaselineItemName = baseline.Name,
CategoryId = baseline.Id,
RecommendationType = recommendationType,
Items = similar
});
}
return recommendations;
@@ -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();
}
}
}
@@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library
if (_config.Configuration.EnableFolderView)
{
var name = _localizationManager.GetLocalizedString("Folders");
var name = _localizationManager.GetServerLocalizedString("Folders");
list.Add(_libraryManager.GetNamedView(name, CollectionType.folders, string.Empty));
}
@@ -168,7 +168,7 @@ namespace Emby.Server.Implementations.Library
public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName)
{
var name = _localizationManager.GetLocalizedString(localizationKey);
var name = _localizationManager.GetServerLocalizedString(localizationKey);
return GetUserSubViewWithName(name, parentId, type, sortName);
}
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library
return GetUserView((Folder)parents[0], viewType, string.Empty);
}
var name = _localizationManager.GetLocalizedString(localizationKey);
var name = _localizationManager.GetServerLocalizedString(localizationKey);
return _libraryManager.GetNamedView(user, name, viewType, sortName);
}
@@ -396,6 +396,12 @@ namespace Emby.Server.Implementations.Library
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.movies);
}
if (collectionType is null)
{
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.unknown);
}
}
return _libraryManager.GetItemList(query, parents);
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
@@ -61,6 +62,9 @@ public class ArtistsValidator
var count = names.Count;
var refreshed = 0;
var liveIds = new HashSet<Guid>();
var unresolved = 0;
foreach (var name in names)
{
try
@@ -73,13 +77,20 @@ public class ArtistsValidator
// Fall back to GetArtist if not found (creates new item if needed)
item ??= _libraryManager.GetArtist(name);
var isNew = !existingArtistIds.Contains(item.Id);
var neverRefreshed = item.DateLastRefreshed == default;
if (isNew || neverRefreshed)
// A name with no item is nothing to refresh, and nothing to keep alive either.
if (item is not null)
{
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
liveIds.Add(item.Id);
var isNew = !existingArtistIds.Contains(item.Id);
var neverRefreshed = item.DateLastRefreshed == default;
if (isNew || neverRefreshed)
{
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
}
}
}
catch (OperationCanceledException)
@@ -88,6 +99,7 @@ public class ArtistsValidator
}
catch (Exception ex)
{
unresolved++;
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
}
@@ -101,13 +113,26 @@ public class ArtistsValidator
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new artists out of {TotalCount} total", refreshed, count);
// Every name that threw is a name whose artist is missing from the live set, and deleting against
// a live set with holes in it deletes artists the library still refers to. Leave the sweep to a
// run that got a clean read of them.
if (unresolved > 0)
{
_logger.LogWarning(
"Not removing dead artists: {Count} of {TotalCount} names could not be resolved this run",
unresolved,
count);
progress.Report(100);
return;
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.MusicArtist],
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>()
.Where(item => item.IsAccessedByName)
}).OfType<MusicArtist>()
.Where(item => item.IsAccessedByName && !liveIds.Contains(item.Id))
.ToList();
foreach (var item in deadEntities)
@@ -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);
}
}
@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
@@ -17,104 +17,143 @@ namespace Emby.Server.Implementations.Library.Validators;
public class PeopleValidator
{
/// <summary>
/// The _library manager.
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The _logger.
/// The logger.
/// </summary>
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
private readonly ILogger<PeopleValidator> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="PeopleValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</param>
public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
public PeopleValidator(ILibraryManager libraryManager, ILogger<PeopleValidator> logger)
{
_libraryManager = libraryManager;
_logger = logger;
_fileSystem = fileSystem;
}
/// <summary>
/// Validates the people.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
// Before the refresh below walks them: a credit no item maps to any more stands for nothing,
// and while it is there the person it names cannot reach the dead-person sweep either.
var numOrphaned = _libraryManager.DeleteOrphanedCredits();
if (numOrphaned > 0)
{
_logger.LogInformation("Deleted {Amount} credits no item maps to", numOrphaned);
}
var names = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
var existingPersonIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Person]
}).ToHashSet();
var (newNames, deadIds) = PartitionCreditsByPersonId(names, _libraryManager.GetPersonId, existingPersonIds);
var numComplete = 0;
var count = names.Count;
var refreshed = 0;
var numPeople = people.Count;
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
_logger.LogDebug("Will refresh {Amount} people", numPeople);
foreach (var person in people)
foreach (var name in names)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var item = _libraryManager.GetPerson(person);
if (item is null)
var item = _libraryManager.GetOrCreatePerson(name);
var isNew = !existingPersonIds.Contains(item.Id);
var neverRefreshed = item.DateLastRefreshed == default;
if (isNew || neverRefreshed)
{
_logger.LogWarning("Failed to get person: {Name}", person);
continue;
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
refreshed++;
}
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
};
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
_logger.LogError(ex, "Error refreshing {PersonName}", name);
}
// Update progress
numComplete++;
double percent = numComplete;
percent /= numPeople;
percent /= count;
percent *= 100;
subProgress.Report(100 * percent);
progress.Report(percent);
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Person],
IsDeadPerson = true,
IsLocked = false
});
_logger.LogInformation(
"Refreshed metadata for {RefreshedCount} people out of {TotalCount} total, {NewCount} of which had no item yet",
refreshed,
count,
newNames.Count);
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
// A person somebody locked is theirs, not ours, however little the library still credits them.
var deadEntities = deadIds
.Select(_libraryManager.GetItemById)
.OfType<Person>()
.Where(item => !item.IsLocked)
.ToList();
var i = 0;
foreach (var item in deadEntities.Chunk(500))
foreach (var item in deadEntities)
{
_libraryManager.DeleteItemsUnsafeFast(item, true);
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
}
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
progress.Report(100);
}
_logger.LogInformation("People validation complete");
/// <summary>
/// Splits the person items into the ones a credit still calls for and the ones nothing does.
/// </summary>
/// <param name="creditNames">Every name credited on an item, from the people table.</param>
/// <param name="getPersonId">Maps a credit name to the id its person item has.</param>
/// <param name="existingPersonIds">The ids of the person items that exist.</param>
/// <returns>The credits needing an item, and the ids of the items nothing credits.</returns>
internal static (List<string> NewNames, List<Guid> DeadIds) PartitionCreditsByPersonId(
IReadOnlyList<string> creditNames,
Func<string, Guid> getPersonId,
IReadOnlySet<Guid> existingPersonIds)
{
ArgumentNullException.ThrowIfNull(creditNames);
ArgumentNullException.ThrowIfNull(getPersonId);
ArgumentNullException.ThrowIfNull(existingPersonIds);
var newNames = new List<string>();
var liveIds = new HashSet<Guid>();
foreach (var name in creditNames)
{
var personId = getPersonId(name);
// Distinct credit names can normalize onto one id; only the first of them needs an item.
if (liveIds.Add(personId) && !existingPersonIds.Contains(personId))
{
newNames.Add(name);
}
}
var deadIds = existingPersonIds.Where(id => !liveIds.Contains(id)).ToList();
return (newNames, deadIds);
}
}
@@ -100,7 +100,7 @@
"TaskAudioNormalization": "تطبيع الصوت",
"TaskAudioNormalizationDescription": "يفحص الملفات لجمع بيانات تطبيع الصوت.",
"TaskDownloadMissingLyrics": "تنزيل الكلمات المفقودة",
"TaskDownloadMissingLyricsDescription": "ينزّل الكلمات للأغاني.",
"TaskDownloadMissingLyricsDescription": "تحميل كلمات الأغاني",
"TaskExtractMediaSegments": "فحص مقاطع المحتوى",
"TaskExtractMediaSegmentsDescription": "يستخرج أو يحصل على مقاطع المحتوى من الملحقات المفعّلة لمقاطع المحتوى (MediaSegment).",
"TaskMoveTrickplayImages": "نقل موقع صور معاينات التنقل",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
"CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل.",
"Original": "فريد",
"LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}"
"LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}",
"NameExtraBehindTheScenes": "خلف المشاهد",
"NameExtraClip": "مقطع",
"NameExtraDeletedScene": "المشهد المحذوف",
"NameExtraInterview": "مقابلة",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "عيّنة",
"NameExtraScene": "مشهد",
"NameExtraShort": "قصير",
"NameExtraThemeSong": "الاغنية السمة",
"NameExtraThemeVideo": "الفيديو السمة",
"NameExtraFeaturette": "فيلم قصير إضافي",
"NameExtraTrailer": "إعلان ترويجي",
"NameExtraUnknown": "إضافي"
}
@@ -106,5 +106,17 @@
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
"CleanupUserDataTask": "Задача па ачыстцы даных карыстальніка",
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён.",
"LyricDownloadFailureFromForItem": "Не ўдалося загрузіць тэкст песні з {0} для {1}",
"NameExtraDeletedScene": "Выдаленая сцэна",
"NameExtraInterview": "Інтэрв'ю",
"NameExtraNumbered": "{0} {1}",
"NameExtraScene": "Сцэна",
"NameExtraTrailer": "Трэйлер",
"NameExtraBehindTheScenes": "За кулісамі",
"NameExtraClip": "Кліп",
"NameExtraFeaturette": "Кароткаметражка",
"NameExtraSample": "Прыклад",
"NameExtraShort": "Кароткаметражка",
"NameExtraThemeSong": "Тэматычная песня"
}
@@ -106,5 +106,20 @@
"TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
"TaskExtractMediaSegments": "Сканиране за сегменти",
"CleanupUserDataTask": "Задача за почистване на потребителски данни",
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни."
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни.",
"LyricDownloadFailureFromForItem": "Текстът на песента не успя да се изтегли от {0} за {1}",
"NameExtraBehindTheScenes": "Зад кулисите",
"NameExtraScene": "Сцена",
"NameExtraShort": "Откъс",
"NameExtraThemeVideo": "Тематично видео",
"NameExtraTrailer": "Трейлър",
"NameExtraUnknown": "Екстра",
"NameExtraClip": "Клип",
"NameExtraDeletedScene": "Изтрита Сцена",
"NameExtraFeaturette": "Кратък филм",
"NameExtraInterview": "Интервю",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Пример",
"NameExtraThemeSong": "Тема-песен",
"Original": "Оригинал"
}
@@ -106,5 +106,17 @@
"TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay",
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana."
"CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana.",
"NameExtraBehindTheScenes": "Iza kulisa",
"NameExtraClip": "Isječak",
"NameExtraDeletedScene": "Izbrišana scena",
"NameExtraFeaturette": "Kratki prilog",
"NameExtraInterview": "Intervju",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Uzorak",
"NameExtraScene": "Scena",
"NameExtraShort": "Kratko",
"NameExtraThemeSong": "Tema",
"NameExtraThemeVideo": "Tematski video",
"NameExtraTrailer": "Najava"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "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",
@@ -51,7 +51,7 @@
"Shows": "Serier",
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
"TvShows": "TV-serier",
"TvShows": "Serier",
"UserCreatedWithName": "Bruger {0} er blevet oprettet",
"UserDeletedWithName": "Brugeren {0} er nu slettet",
"UserDownloadingItemWithValues": "{0} henter {1}",
@@ -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",
"TaskCleanTranscodeDescription": "Fjerner omkodningsfiler, som er mere end 1 dag gamle.",
"TaskCleanTranscode": "Tøm omkodningsmappen",
"TaskRefreshPeople": "Opdater personer",
"TaskRefreshChannels": "Opdatér kanaler",
"TaskCleanTranscodeDescription": "Fjerner transkodningsfiler, som er mere end 1 dag gamle.",
"TaskCleanTranscode": "Tøm transkodningsmappen",
"TaskRefreshPeople": "Opdatér personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
"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": "Temasang",
"NameExtraThemeVideo": "Temavideo",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Ekstra"
}
@@ -1,6 +1,6 @@
{
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Artists": "Interpreten",
"Artists": "Künstler",
"AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
"Books": "Bücher",
"ChapterNameValue": "Kapitel {0}",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}"
"LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}",
"NameExtraBehindTheScenes": "Behind The Scenes",
"NameExtraDeletedScene": "Entfernte Szene",
"NameExtraInterview": "Interview",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Ausschnitt",
"NameExtraScene": "Szene",
"NameExtraShort": "Kurzfilm",
"NameExtraThemeSong": "Titellied",
"NameExtraThemeVideo": "Titelvideo",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra",
"NameExtraClip": "Clip",
"NameExtraFeaturette": "Hinter den Kulissen"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Καθαρίζει όλα τα δεδομένα χρήστη (κατάσταση παρακολούθησης, κατάσταση αγαπημένων κ.λπ.) από πολυμέσα που δεν υπάρχουν πλέον για τουλάχιστον 90 ημέρες.",
"CleanupUserDataTask": "Εργασία εκκαθάρισης δεδομένων χρήστη",
"LyricDownloadFailureFromForItem": "Αποτυχία λήψης στίχων από {0} για {1}",
"Original": "Πρωτότυπο"
"Original": "Πρωτότυπο",
"NameExtraBehindTheScenes": "Πίσω από τις Σκηνές",
"NameExtraDeletedScene": "Διεγραμμένη Σκηνή",
"NameExtraFeaturette": "Πρόσθετα βίντεο",
"NameExtraInterview": "Συνέντευξη",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Δείγμα",
"NameExtraScene": "Σκηνή",
"NameExtraShort": "Βίντεο μικρού μήκους",
"NameExtraThemeSong": "Θεματικό Τραγούδι",
"NameExtraThemeVideo": "Θεματικό Βίντεο",
"NameExtraTrailer": "τρέιλερ ταινίας",
"NameExtraUnknown": "Πρόσθετα",
"NameExtraClip": "Απόσπασμα"
}
@@ -24,8 +24,8 @@
"Music": "Music",
"MusicVideos": "Music Videos",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",
"NameSeasonNumber": "Series {0}",
"NameSeasonUnknown": "Series Unknown",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
"NotificationOptionApplicationUpdateAvailable": "Application update available",
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "User data cleanup task",
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days.",
"LyricDownloadFailureFromForItem": "Lyrics failed to download from {0} for {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Behind The Scenes",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Deleted Scene",
"NameExtraFeaturette": "Featurette",
"NameExtraInterview": "Interview",
"NameExtraSample": "Sample",
"NameExtraScene": "Scene",
"NameExtraShort": "Short",
"NameExtraThemeSong": "Theme Song",
"NameExtraThemeVideo": "Theme Video",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra",
"NameExtraNumbered": "{0} {1}"
}
@@ -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",
@@ -97,5 +97,9 @@
"TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.",
"TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)",
"TaskAudioNormalization": "Normaligo Sonnivela",
"HearingImpaired": "Surda"
"HearingImpaired": "Surda",
"NameExtraDeletedScene": "Forigita sceno",
"NameExtraScene": "Sceno",
"NameExtraThemeSong": "Tema Kanto",
"Original": "Originala"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, estado de los favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días.",
"CleanupUserDataTask": "Tarea de limpieza de datos de usuarios",
"LyricDownloadFailureFromForItem": "No se pudo descargar la letra desde {0} para {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Detrás de cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Minidocumental",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Música de presentación",
"NameExtraThemeVideo": "Video de presentación",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra"
}
@@ -106,5 +106,20 @@
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
"CleanupUserDataTask": "Tarea de limpieza de los datos del usuario",
"CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días."
"CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días.",
"LyricDownloadFailureFromForItem": "No se pudo descargar la letra desde {0} para {1}",
"NameExtraBehindTheScenes": "Detrás de cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Minidocumental",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Música de presentación",
"NameExtraThemeVideo": "Video de presentación",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra",
"Original": "Original"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Tarea de limpieza de datos del usuario",
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}"
"LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}",
"NameExtraBehindTheScenes": "Detrás de Cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Reportaje especial",
"NameExtraInterview": "Entrevista",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Tema principal",
"NameExtraThemeVideo": "Vídeo del tema principal",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra",
"NameExtraNumbered": "{0} {1}"
}
@@ -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,18 @@
"CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.",
"CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina",
"LyricDownloadFailureFromForItem": "Ezin izan dira {1}-ren letrak deskargatu {0}-tik",
"Original": "Jatorrizkoa"
"Original": "Jatorrizkoa",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Lagina",
"NameExtraScene": "Eszena",
"NameExtraShort": "Laburra",
"NameExtraThemeSong": "Gai-abestia",
"NameExtraThemeVideo": "Gai-bideoa",
"NameExtraBehindTheScenes": "Eszenen atzean",
"NameExtraClip": "Klipa",
"NameExtraDeletedScene": "Ezabatutako eszena",
"NameExtraFeaturette": "Erreportajea",
"NameExtraInterview": "Elkarrizketa",
"NameExtraTrailer": "Trailerra",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä",
"CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään.",
"LyricDownloadFailureFromForItem": "Sanoitusten lataus kohteesta {0} kappaleelle {1} epäonnistui",
"Original": "Alkuperäinen"
"Original": "Alkuperäinen",
"NameExtraBehindTheScenes": "Kulissien Takana",
"NameExtraClip": "Klippi",
"NameExtraDeletedScene": "Poistettu Kohtaus",
"NameExtraFeaturette": "Lyhytelokuva",
"NameExtraInterview": "Haastattelu",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Näyte",
"NameExtraScene": "Kohtaus",
"NameExtraShort": "Lyhytfilmi",
"NameExtraThemeSong": "Tunnusmusiikki",
"NameExtraThemeVideo": "Tunnusvideo",
"NameExtraTrailer": "Traileri"
}
@@ -1,16 +1,125 @@
{
"Artists": "Listafólk",
"Collections": "Søvn",
"Default": "Sjálvgildi",
"Artists": "Tónlistafólk",
"Collections": "Samlingar",
"Default": "Forsett",
"External": "Ytri",
"Genres": "Greinar",
"AppDeviceValues": "App: {0}, Eind: {1}",
"Books": "Bøkur",
"ChapterNameValue": "Kapittul {0}",
"Favorites": "Yndis",
"Folders": "Mappur",
"Favorites": "Yndislista",
"Folders": "Skjáttur",
"Forced": "Kravt",
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
"HeaderFavoriteEpisodes": "Yndispartar",
"LabelIpAddressValue": "IP atsetur: {0}"
"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": "Umfar {0}",
"NameSeasonUnknown": "Ókent umfar",
"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": "Brellbiti",
"NameExtraInterview": "Samrøða",
"NameExtraBehindTheScenes": "Aftanfyri leiktjøldini",
"NameExtraClip": "Klipp",
"NameExtraNumbered": "{0} {1}",
"NameExtraFeaturette": "Stuttur heimildarfilmur",
"TaskAudioNormalization": "Ljóðjavnan",
"TaskAudioNormalizationDescription": "Kannar fílur fyri dátum til ljóðjavnan.",
"NameExtraSample": "Sýnislutur",
"TaskRefreshTrickplayImages": "Framleið Trickplay-myndir",
"TaskRefreshTrickplayImagesDescription": "Framleiðir trickplay-myndir fyri kykmyndir í søvnunm har tað er virkt.",
"TaskMoveTrickplayImages": "Flyt Trickplay-myndagoymslustað",
"TaskMoveTrickplayImagesDescription": "Flytur verandi trickplay-fílur sambært savnsstillingunum.",
"NameExtraThemeVideo": "Eyðkenniskykmynd",
"NameExtraDeletedScene": "Úrtikin mynd",
"NameExtraScene": "Mynd",
"NameExtraUnknown": "Eykatilfar",
"Original": "Upprunalig(t/ur)"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles a échoué de {0} pour {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Dans Les Coulisses",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Scène supprimée",
"NameExtraFeaturette": "Court-métrage",
"NameExtraInterview": "Entrevue",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Échantillon",
"NameExtraScene": "Scène",
"NameExtraShort": "Court-métrage",
"NameExtraThemeSong": "Chanson thème",
"NameExtraThemeVideo": "Générique",
"NameExtraTrailer": "Bande-annonce",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles à échoué de {0} pour {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Dans Les Coulisses",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Scène supprimée",
"NameExtraFeaturette": "Court-métrage",
"NameExtraInterview": "Entrevue",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Échantillon",
"NameExtraScene": "Scène",
"NameExtraShort": "Court-métrage",
"NameExtraThemeSong": "Thème musical",
"NameExtraThemeVideo": "Générique",
"NameExtraTrailer": "Bande-annonce",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,17 @@
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad.",
"Original": "Bunaidh",
"LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}"
"LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}",
"NameExtraBehindTheScenes": "Taobh thiar de na Radhairc",
"NameExtraClip": "Gearrthóg",
"NameExtraDeletedScene": "Radharc Scriosta",
"NameExtraFeaturette": "Mionghné",
"NameExtraInterview": "Agallamh",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Sampla",
"NameExtraScene": "Radharc",
"NameExtraShort": "Gearr",
"NameExtraThemeSong": "Amhrán Téama",
"NameExtraThemeVideo": "Físeán Téama",
"NameExtraTrailer": "Leantóir"
}
@@ -107,5 +107,19 @@
"TaskAudioNormalizationDescription": "Escanea ficheiros á procura de datos de normalización de volume.",
"CleanupUserDataTask": "Tarefa de limpeza de datos dos usuarios",
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (estado de visualización, de favorito etc.) dos medios ausentes polo menos 90 días.",
"Original": "Orixinal"
"Original": "Orixinal",
"LyricDownloadFailureFromForItem": "Non se puideron descargar as letras desde {0} para {1}",
"NameExtraBehindTheScenes": "Detrás das Cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena Eliminada",
"NameExtraFeaturette": "Reportaxe especial",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Mostra",
"NameExtraScene": "Escena",
"NameExtraShort": "Curtametraxe",
"NameExtraThemeSong": "Canción principal",
"NameExtraThemeVideo": "Vídeo da canción principal",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra"
}
@@ -106,5 +106,14 @@
"TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.",
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
"CleanupUserDataTask": "משימת ניקוי מידע משתמש"
"CleanupUserDataTask": "משימת ניקוי מידע משתמש",
"LyricDownloadFailureFromForItem": "הורדת המילים מ-{0} עבור {1} נכשלה",
"Original": "מקור",
"NameExtraBehindTheScenes": "מאחורי הקלעים",
"NameExtraClip": "קליפ",
"NameExtraDeletedScene": "סצנה שנמחקה",
"NameExtraFeaturette": "סרט קצר",
"NameExtraInterview": "ריאיון",
"NameExtraSample": "דגימה",
"NameExtraScene": "סצנה"
}
@@ -105,5 +105,21 @@
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य"
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य",
"Original": "असली",
"LyricDownloadFailureFromForItem": "{0} के लिए {1} से बोल (Lyrics) डाउनलोड करने में विफल रहा",
"NameExtraBehindTheScenes": "परदे के पीछे",
"NameExtraClip": "क्लिप",
"NameExtraDeletedScene": "हटाया गया दृश्य",
"NameExtraFeaturette": "फीचरेट",
"NameExtraInterview": "साक्षात्कार",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "नमूना",
"NameExtraScene": "दृश्य",
"NameExtraShort": "शॉर्ट",
"NameExtraThemeSong": "थीम सॉन्ग",
"NameExtraThemeVideo": "थीम वीडियो",
"NameExtraTrailer": "ट्रेलर",
"NameExtraUnknown": "अतिरिक्त",
"CleanupUserDataTaskDescription": "कम से कम 90 दिनों से अनुपस्थित मीडिया से सभी उपयोगकर्ता डेटा (देखने की स्थिति, पसंदीदा स्थिति आदि) को साफ़ करता है।"
}
@@ -107,5 +107,18 @@
"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",
"NameExtraBehindTheScenes": "Iza kulisa",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Obrisana Scena",
"NameExtraFeaturette": "Promotivni video",
"NameExtraInterview": "Intervju",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Uzorak",
"NameExtraScene": "Scena",
"NameExtraShort": "Kratki film",
"NameExtraThemeSong": "Glavna Pjesma",
"NameExtraThemeVideo": "Tema videa",
"NameExtraTrailer": "Trailer"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.",
"CleanupUserDataTask": "Felhasználói adatok tisztítása feladat",
"Original": "Eredeti",
"LyricDownloadFailureFromForItem": "Dalszöveg letöltése {0}-tól {1}-hez sikertelen"
"LyricDownloadFailureFromForItem": "Dalszöveg letöltése {0}-tól {1}-hez sikertelen",
"NameExtraBehindTheScenes": "Színfalak mögött",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Törölt jelenet",
"NameExtraFeaturette": "Kísérő film",
"NameExtraInterview": "Interjú",
"NameExtraSample": "Minta",
"NameExtraScene": "Jelenet",
"NameExtraShort": "Rövidfilm",
"NameExtraThemeSong": "Főcímdal",
"NameExtraThemeVideo": "Főcímvideó",
"NameExtraTrailer": "Előzetes",
"NameExtraUnknown": "Extra",
"NameExtraNumbered": "{0} {1}"
}
@@ -24,5 +24,10 @@
"TaskDownloadMissingSubtitles": "Ներբեռնել պակասող ենթագրերը",
"AppDeviceValues": "Հավելված` {0}, Սարք `{1}",
"ChapterNameValue": "Գլուխ {0}",
"Collections": "Հավաքածուներ"
"Collections": "Հավաքածուներ",
"Artists": "Երաժիշտներ",
"Default": "Լռելյայն",
"Favorites": "Ընտրյալ",
"Forced": "Ստիպուած",
"Genres": "Ոճ"
}
@@ -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,18 @@
"CleanupUserDataTask": "Task di pulizia dei dati utente",
"CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni.",
"Original": "Originale",
"LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}"
"LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}",
"NameExtraBehindTheScenes": "Dietro le scene",
"NameExtraClip": "Filmato",
"NameExtraDeletedScene": "Scena eliminata",
"NameExtraInterview": "Intervista",
"NameExtraNumbered": "{0} {1}",
"NameExtraScene": "Scena",
"NameExtraSample": "Campione",
"NameExtraShort": "Corto",
"NameExtraThemeSong": "Sigla musicale",
"NameExtraTrailer": "Trailer",
"NameExtraFeaturette": "Caratteristica",
"NameExtraThemeVideo": "Video tematico",
"NameExtraUnknown": "Extra"
}
@@ -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,21 @@
"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.",
"NameExtraBehindTheScenes": "Hannert de Kulissen",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Geläschte Scène",
"NameExtraFeaturette": "Featurette",
"NameExtraInterview": "Interview",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Beispill",
"NameExtraScene": "Scène",
"NameExtraShort": "Kuerzfilm",
"NameExtraThemeSong": "Theme-Lidd",
"NameExtraThemeVideo": "Theme-Video",
"NameExtraTrailer": "Bande-Annonce"
}
@@ -3,15 +3,15 @@
"Artists": "Atlikėjai",
"AuthenticationSucceededWithUserName": "{0} sėkmingai autentifikuota",
"Books": "Knygos",
"ChapterNameValue": "Scena{0}",
"Collections": "Rinkiniai",
"ChapterNameValue": "Skyrius{0}",
"Collections": "Kolekcijos",
"FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
"Favorites": "Mėgstami",
"Folders": "Katalogai",
"Folders": "Aplankai",
"Genres": "Žanrai",
"HeaderContinueWatching": "Žiūrėti toliau",
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
"HeaderFavoriteEpisodes": "Mėgstami Epizodai",
"HeaderFavoriteShows": "Mėgstamos TV Laidos",
"HeaderLiveTV": "Tiesioginė TV",
"HeaderNextUp": "Toliau",
"HomeVideos": "Namų vaizdo įrašai",
@@ -26,14 +26,14 @@
"NameInstallFailed": "{0} diegimo klaida",
"NameSeasonNumber": "Sezonas {0}",
"NameSeasonUnknown": "Sezonas neatpažintas",
"NewVersionIsAvailable": "Nauja \"Jellyfin Server\" versija yra prieinama atsisiuntimui.",
"NewVersionIsAvailable": "Nauja Jellyfin Server versija yra prieinama atsisiuntimui.",
"NotificationOptionApplicationUpdateAvailable": "Galimi programos atnaujinimai",
"NotificationOptionApplicationUpdateInstalled": "Programos atnaujinimai įdiegti",
"NotificationOptionAudioPlayback": "Garso atkūrimas pradėtas",
"NotificationOptionAudioPlaybackStopped": "Garso atkūrimas sustabdytas",
"NotificationOptionCameraImageUploaded": "Kameros vaizdai įkelti",
"NotificationOptionCameraImageUploaded": "Kameros atvaizdai įkelti",
"NotificationOptionInstallationFailed": "Diegimo klaida",
"NotificationOptionNewLibraryContent": "Naujas turinys įkeltas",
"NotificationOptionNewLibraryContent": "Pridėtas naujas turinys",
"NotificationOptionPluginError": "Įskiepio klaida",
"NotificationOptionPluginInstalled": "Įskiepis įdiegtas",
"NotificationOptionPluginUninstalled": "Įskiepis išdiegtas",
@@ -51,7 +51,7 @@
"Shows": "Laidos",
"StartupEmbyServerIsLoading": "Jellyfin Server kraunasi. Netrukus pabandykite dar kartą.",
"SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
"TvShows": "TV laidos",
"TvShows": "TV Laidos",
"UserCreatedWithName": "Buvo sukurtas {0} naudotojas",
"UserDeletedWithName": "Naudotojas {0} ištrintas",
"UserDownloadingItemWithValues": "{0} siunčiasi {1}",
@@ -59,14 +59,14 @@
"UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
"UserOnlineFromDevice": "{0} prisijungęs iš {1}",
"UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
"UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
"UserStartedPlayingItemWithValues": "{0} atkuriama {1} į {2}",
"UserStoppedPlayingItemWithValues": "{0} baigė atkūrimą {1} į {2}",
"VersionNumber": "Versija {0}",
"TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
"TaskUpdatePlugins": "Atnaujinti įskieius",
"TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
"TaskCleanTranscode": "Išvalyti perkodavimo katalogą",
"TaskCleanTranscodeDescription": "Ištrina dienos senumo transkodavimo failus.",
"TaskCleanTranscode": "Išvalyti transkodavimo katalogą",
"TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.",
"TaskRefreshLibrary": "Skenuoti medijos biblioteką",
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
@@ -76,8 +76,8 @@
"TaskRefreshPeople": "Atnaujinti žmones",
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
"TaskCleanLogs": "Išvalyti žurnalą",
"TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.",
"TaskRefreshChapterImages": "Ištraukti skyrių vaizdus",
"TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašams, kuriuose yra skyriai.",
"TaskRefreshChapterImages": "Ištraukti skyrių atvaizdus",
"TaskCleanCache": "Išvalyti talpyklą",
"TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
"TasksChannelsCategory": "Internetiniai kanalai",
@@ -87,7 +87,7 @@
"TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
"Undefined": "Neapibrėžtas",
"Forced": "Priverstinis",
"Default": "Numatytas",
"Default": "Numatytasis",
"TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.",
"TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
"TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
@@ -96,15 +96,30 @@
"External": "Išorinis",
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
"TaskRefreshTrickplayImagesDescription": "Sukuria vaizdo įrašų, esančių įgalintose bibliotekose, Trickplay peržiūras.",
"TaskAudioNormalization": "Garso normalizavimas",
"TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.",
"TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
"TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
"TaskDownloadMissingLyrics": "Atsisiųsti trūkstamus dainų tekstus",
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.",
"TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
"TaskMoveTrickplayImages": "Pakeisti Trickplay atvaizdų vietą",
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius Trickplay failus pagal bibliotekos nustatymus.",
"TaskDownloadMissingLyricsDescription": "Atsisiųsti dainų tekstus",
"CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)."
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamą būseną ir t. t.).",
"LyricDownloadFailureFromForItem": "Nepavyko atsisiųsti dainos teksto iš {0}, skirto {1}",
"NameExtraBehindTheScenes": "Užkulisiuose",
"NameExtraClip": "Klipas",
"NameExtraDeletedScene": "Ištrinta scena",
"NameExtraInterview": "Interviu",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Pavyzdys",
"NameExtraScene": "Scena",
"NameExtraThemeSong": "Teminė daina",
"NameExtraThemeVideo": "Teminis vaizdo įrašas",
"NameExtraTrailer": "Anonsas",
"NameExtraUnknown": "Papildomas",
"Original": "Originalus",
"NameExtraFeaturette": "Trumpametražis filmas",
"NameExtraShort": "Trumpas filmukas"
}
@@ -107,5 +107,18 @@
"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}",
"NameExtraBehindTheScenes": "Aiz kadra",
"NameExtraClip": "Klips",
"NameExtraDeletedScene": "Izdzēsta aina",
"NameExtraFeaturette": "Īsfilma",
"NameExtraInterview": "Intervija",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Paraugs",
"NameExtraScene": "Aina",
"NameExtraShort": "Īsfilma",
"NameExtraThemeSong": "Motīvu dziesma",
"NameExtraThemeVideo": "Tēmas video",
"NameExtraTrailer": "Treileris"
}
@@ -106,5 +106,11 @@
"TaskAudioNormalization": "Normalisasi Audio",
"TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.",
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (keadaan tontonan, status kegemaran, dan sebagainya) daripada media yang tidak lagi wujud sekurang-kurangnya selama 90 hari.",
"CleanupUserDataTask": "Tugas pembersihan data pengguna"
"CleanupUserDataTask": "Tugas pembersihan data pengguna",
"LyricDownloadFailureFromForItem": "Lirik gagal dimuat turun dari {0} untuk {1}",
"NameExtraBehindTheScenes": "Di Sebalik Takbir",
"NameExtraClip": "Klip",
"NameExtraInterview": "Temu bual",
"NameExtraThemeSong": "Lagu Tema",
"NameExtraThemeVideo": "Video Tema"
}
@@ -106,5 +106,15 @@
"TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.",
"TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment.",
"CleanupUserDataTaskDescription": "Sletter all brukerdata (avspillings-status, favoritter osv.) fra innhold som har vært utilgjengelig i minst 90 dager.",
"CleanupUserDataTask": "Oppgave for opprydding av brukerdata"
"CleanupUserDataTask": "Oppgave for opprydding av brukerdata",
"NameExtraBehindTheScenes": "Bak kulissene",
"NameExtraDeletedScene": "Slettet scene",
"NameExtraFeaturette": "Presentasjonsfilm",
"NameExtraInterview": "Intervju",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Prøve",
"NameExtraScene": "Scene",
"NameExtraThemeSong": "Tema-låt",
"NameExtraThemeVideo": "Tema-video",
"NameExtraTrailer": "Trailer"
}
@@ -108,5 +108,18 @@
"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,18 @@
"CleanupUserDataTask": "Tarefa de limpeza de dados do usuário",
"CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias.",
"LyricDownloadFailureFromForItem": "Download das Letras falharam em {0} para o item {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Nos Bastidores",
"NameExtraClip": "Clipe",
"NameExtraDeletedScene": "cena Extra",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Trecho",
"NameExtraScene": "Cena",
"NameExtraShort": "Curta-metragem",
"NameExtraThemeSong": "Música Tema",
"NameExtraThemeVideo": "Vídeo de Abertura",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra",
"NameExtraFeaturette": "Nos Bastidores",
"NameExtraInterview": "Entrevista"
}
@@ -5,108 +5,121 @@
"Books": "Livros",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Coleções",
"FailedLoginAttemptWithUserName": "Tentativa de login falhada a partir de {0}",
"FailedLoginAttemptWithUserName": "Tentativa falhada de início de sessão do utilizador {0}",
"Favorites": "Favoritos",
"Folders": "Pastas",
"Genres": "Géneros",
"HeaderContinueWatching": "Continuar a ver",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
"HeaderLiveTV": "TV em Direto",
"HeaderNextUp": "A Seguir",
"HomeVideos": "Vídeos Caseiros",
"HeaderFavoriteEpisodes": "Episódios favoritos",
"HeaderFavoriteShows": "Séries favoritas",
"HeaderLiveTV": "TV em direto",
"HeaderNextUp": "A seguir",
"HomeVideos": "Vídeos caseiros",
"Inherit": "Herdar",
"LabelIpAddressValue": "Endereço IP: {0}",
"LabelRunningTimeValue": "Duração: {0}",
"Latest": "Mais Recente",
"MixedContent": "Conteúdo Misto",
"Latest": "Mais recente",
"MixedContent": "Conteúdo misto",
"Movies": "Filmes",
"Music": "Música",
"MusicVideos": "Videoclipes",
"NameInstallFailed": "{0} falha na instalação",
"NameInstallFailed": "Falha na instalação de {0}",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconhecida",
"NameSeasonUnknown": "Temporada desconhecida",
"NewVersionIsAvailable": "Está disponível para transferência uma nova versão do servidor Jellyfin.",
"NotificationOptionApplicationUpdateAvailable": "Atualização de aplicação disponível",
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicação instalada",
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
"NotificationOptionAudioPlayback": "Reprodução de áudio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reprodução de áudio interrompida",
"NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
"NotificationOptionInstallationFailed": "Falha na instalação",
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
"NotificationOptionPluginError": "Falha na extensão",
"NotificationOptionPluginInstalled": "Extensão instalada",
"NotificationOptionPluginUninstalled": "Extensão desinstalada",
"NotificationOptionPluginUpdateInstalled": "Extensão atualizada",
"NotificationOptionPluginError": "Falha no plugin",
"NotificationOptionPluginInstalled": "Plugin instalado",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
"NotificationOptionPluginUpdateInstalled": "Atualização de plugin instalada",
"NotificationOptionServerRestartRequired": "Necessário reiniciar o servidor",
"NotificationOptionTaskFailed": "Falha em tarefa agendada",
"NotificationOptionUserLockedOut": "Utilizador bloqueado",
"NotificationOptionVideoPlayback": "Reprodução do vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reprodução do vídeo parada",
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo interrompida",
"Photos": "Fotografias",
"PluginInstalledWithName": "{0} foi instalado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginUpdatedWithName": "{0} foi atualizado",
"ScheduledTaskFailedWithName": "{0} falhou",
"Shows": "Séries",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente mais tarde.",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}",
"TvShows": "Séries",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tenta novamente dentro de instantes.",
"SubtitleDownloadFailureFromForItem": "Falha ao transferir legendas de {0} para {1}",
"TvShows": "Séries de TV",
"UserCreatedWithName": "Utilizador {0} criado",
"UserDeletedWithName": "Utilizador {0} apagado",
"UserDeletedWithName": "Utilizador {0} eliminado",
"UserDownloadingItemWithValues": "{0} está a transferir {1}",
"UserLockedOutWithName": "Utilizador {0} bloqueado",
"UserOfflineFromDevice": "{0} desligou-se a partir de {1}",
"UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
"UserOfflineFromDevice": "{0} desligou-se de {1}",
"UserOnlineFromDevice": "{0} está online em {1}",
"UserPasswordChangedWithName": "Palavra-passe alterada para o utilizador {0}",
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
"VersionNumber": "Versão {0}",
"TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas em falta baseado na configuração de metadados.",
"TaskDownloadMissingSubtitlesDescription": "Procura na Internet legendas em falta com base na configuração dos metadados.",
"TaskDownloadMissingSubtitles": "Transferir legendas em falta",
"TaskRefreshChannelsDescription": "Atualizar informação sobre canais da Internet.",
"TaskRefreshChannels": "Atualizar Canais",
"TaskCleanTranscodeDescription": "Apagar ficheiros de transcode com mais de um dia.",
"TaskCleanTranscode": "Limpar a Diretoria de Transcode",
"TaskUpdatePluginsDescription": "Faz o download e instala updates para os plugins que estão configurados para atualizar automaticamente.",
"TaskUpdatePlugins": "Atualizar Plugins",
"TaskRefreshPeopleDescription": "Atualizar metadados para elenco e equipa técnica da tua mediateca.",
"TaskRefreshPeople": "Atualizar Pessoas",
"TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.",
"TaskCleanLogs": "Limpar a Diretoria de Logs",
"TaskRefreshLibraryDescription": "Analisar a mediateca para novos ficheiros e atualizar os metadados.",
"TaskRefreshLibrary": "Analisar mediateca",
"TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.",
"TaskRefreshChapterImages": "Extrair Imagens dos Capítulos",
"TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.",
"TaskCleanCache": "Limpar Cache",
"TaskRefreshChannelsDescription": "Atualiza as informações dos canais da Internet.",
"TaskRefreshChannels": "Atualizar canais",
"TaskCleanTranscodeDescription": "Elimina ficheiros de transcodificação com mais de um dia.",
"TaskCleanTranscode": "Limpar pasta de transcodificação",
"TaskUpdatePluginsDescription": "Transfere e instala atualizações dos plugins configurados para atualização automática.",
"TaskUpdatePlugins": "Atualizar plugins",
"TaskRefreshPeopleDescription": "Atualiza os metadados de atores e realizadores na tua biblioteca multimédia.",
"TaskRefreshPeople": "Atualizar pessoas",
"TaskCleanLogsDescription": "Elimina ficheiros de registo com mais de {0} dias.",
"TaskCleanLogs": "Limpar pasta de registos",
"TaskRefreshLibraryDescription": "Analisa a biblioteca multimédia à procura de novos ficheiros e atualiza os metadados.",
"TaskRefreshLibrary": "Analisar biblioteca multimédia",
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
"TaskRefreshChapterImages": "Extrair imagens dos capítulos",
"TaskCleanCacheDescription": "Elimina ficheiros de cache que já não são necessários ao sistema.",
"TaskCleanCache": "Limpar pasta de cache",
"TasksChannelsCategory": "Canais da Internet",
"TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Mediateca",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskCleanActivityLogDescription": "Apaga as entradas do registo de atividade anteriores à data configurada.",
"TaskCleanActivityLogDescription": "Elimina as entradas do registo de atividade mais antigas do que o período configurado.",
"TaskCleanActivityLog": "Limpar registo de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
"Default": "Predefinição",
"TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
"TaskOptimizeDatabaseDescription": "Compacta a base de dados e liberta espaço não utilizado. A execução desta tarefa depois de analisar a biblioteca ou de outras alterações que modifiquem a base de dados pode melhorar o desempenho.",
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
"TaskKeyframeExtractorDescription": "Extrai fotogramas-chave de ficheiros de vídeo para criar playlists HLS mais precisas. Esta tarefa pode demorar algum tempo.",
"TaskKeyframeExtractor": "Extrator de fotogramas-chave",
"External": "Externo",
"HearingImpaired": "Surdo",
"HearingImpaired": "Deficiência auditiva",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
"TaskAudioNormalization": "Normalização de áudio",
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
"TaskDownloadMissingLyrics": "Transferir letra em falta",
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
"TaskExtractMediaSegments": "Analisar segmentos multimédia",
"TaskDownloadMissingLyrics": "Transferir letras em falta",
"TaskMoveTrickplayImages": "Migrar localização das imagens de trickplay",
"TaskDownloadMissingLyricsDescription": "Transfere letras de músicas",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos multimédia de plugins com MediaSegment ativado.",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de trickplay existentes de acordo com as definições da biblioteca.",
"CleanupUserDataTaskDescription": "Remove todos os dados de utilizador (estado de reprodução, estado de favorito, etc.) de conteúdos multimédia que já não estejam presentes há pelo menos 90 dias.",
"CleanupUserDataTask": "Limpeza de dados de utilizador",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
"LyricDownloadFailureFromForItem": "Falha ao transferir letras de {0} para {1}",
"NameExtraDeletedScene": "Cena eliminada",
"NameExtraInterview": "Entrevista",
"NameExtraUnknown": "Extra",
"NameExtraBehindTheScenes": "Bastidores",
"NameExtraSample": "Amostra",
"NameExtraScene": "Cena",
"NameExtraClip": "Excerto",
"NameExtraFeaturette": "Minidocumentário",
"NameExtraThemeSong": "Tema musical",
"NameExtraThemeVideo": "Vídeo temático",
"NameExtraShort": "Curta-metragem",
"NameExtraNumbered": "{0} {1}",
"NameExtraTrailer": "Trailer"
}
@@ -1,112 +1,125 @@
{
"HeaderLiveTV": "TV Em Direto",
"HeaderLiveTV": "TV em direto",
"Collections": "Coleções",
"Books": "Livros",
"Artists": "Artistas",
"HeaderNextUp": "A Seguir",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
"HeaderNextUp": "A seguir",
"HeaderFavoriteEpisodes": "Episódios favoritos",
"HeaderFavoriteShows": "Séries favoritas",
"HeaderContinueWatching": "Continuar a ver",
"Genres": "Géneros",
"Folders": "Pastas",
"Favorites": "Favoritos",
"UserDownloadingItemWithValues": "{0} está sendo baixado {1}",
"UserDownloadingItemWithValues": "{0} está a transferir {1}",
"VersionNumber": "Versão {0}",
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
"UserStartedPlayingItemWithValues": "{0} está reproduzindo {1} em {2}",
"UserPasswordChangedWithName": "A senha do usuário {0} foi alterada",
"UserOnlineFromDevice": "{0} está online a partir de {1}",
"UserOfflineFromDevice": "{0} desconectou-se a partir de {1}",
"UserLockedOutWithName": "O usuário {0} foi bloqueado",
"UserDeletedWithName": "O usuário {0} foi removido",
"UserCreatedWithName": "O usuário {0} foi criado",
"TvShows": "Séries",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está iniciando. Tente novamente dentro de momentos.",
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
"UserPasswordChangedWithName": "Palavra-passe alterada para o utilizador {0}",
"UserOnlineFromDevice": "{0} está online em {1}",
"UserOfflineFromDevice": "{0} desligou-se de {1}",
"UserLockedOutWithName": "Utilizador {0} bloqueado",
"UserDeletedWithName": "Utilizador {0} eliminado",
"UserCreatedWithName": "Utilizador {0} criado",
"TvShows": "Séries de TV",
"SubtitleDownloadFailureFromForItem": "Falha ao transferir legendas de {0} para {1}",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tenta novamente dentro de instantes.",
"ScheduledTaskFailedWithName": "{0} falhou",
"PluginUpdatedWithName": "{0} foi atualizado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginInstalledWithName": "{0} foi instalado",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo interrompida",
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
"NotificationOptionUserLockedOut": "Usuário bloqueado",
"NotificationOptionTaskFailed": "Falha na tarefa agendada",
"NotificationOptionServerRestartRequired": "É necessário reiniciar o servidor",
"NotificationOptionPluginUpdateInstalled": "Plugin atualizado",
"NotificationOptionUserLockedOut": "Utilizador bloqueado",
"NotificationOptionTaskFailed": "Falha em tarefa agendada",
"NotificationOptionServerRestartRequired": "Necessário reiniciar o servidor",
"NotificationOptionPluginUpdateInstalled": "Atualização de plugin instalada",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
"NotificationOptionPluginInstalled": "Plugin instalado",
"NotificationOptionPluginError": "Falha no plugin",
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
"NotificationOptionInstallationFailed": "Falha de instalação",
"NotificationOptionCameraImageUploaded": "Imagem de câmera enviada",
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
"NotificationOptionApplicationUpdateInstalled": "A atualização do aplicativo foi instalada",
"NotificationOptionApplicationUpdateAvailable": "Uma atualização do aplicativo está disponível",
"NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.",
"NameSeasonUnknown": "Temporada Desconhecida",
"NotificationOptionInstallationFailed": "Falha na instalação",
"NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
"NotificationOptionAudioPlaybackStopped": "Reprodução de áudio interrompida",
"NotificationOptionAudioPlayback": "Reprodução de áudio iniciada",
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicação instalada",
"NotificationOptionApplicationUpdateAvailable": "Atualização de aplicação disponível",
"NewVersionIsAvailable": "Está disponível para transferência uma nova versão do servidor Jellyfin.",
"NameSeasonUnknown": "Temporada desconhecida",
"NameSeasonNumber": "Temporada {0}",
"NameInstallFailed": "Falha na instalação de {0}",
"MusicVideos": "Videoclipes",
"Music": "Música",
"MixedContent": "Conteúdo diverso",
"Latest": "Mais Recente",
"MixedContent": "Conteúdo misto",
"Latest": "Mais recente",
"LabelRunningTimeValue": "Duração: {0}",
"LabelIpAddressValue": "Endereço de IP: {0}",
"LabelIpAddressValue": "Endereço IP: {0}",
"Inherit": "Herdar",
"HomeVideos": "Vídeos Caseiros",
"HomeVideos": "Vídeos caseiros",
"Shows": "Séries",
"Photos": "Fotografias",
"Movies": "Filmes",
"FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}",
"FailedLoginAttemptWithUserName": "Tentativa falhada de início de sessão do utilizador {0}",
"ChapterNameValue": "Capítulo {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
"TaskCleanCache": "Limpar pasta de cache",
"TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Mediateca",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskRefreshChannels": "Atualizar Canais",
"TaskUpdatePlugins": "Atualizar Plugins",
"TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
"TaskCleanLogs": "Limpar diretório de logs",
"TaskRefreshLibrary": "Analisar mediateca",
"TaskRefreshChannels": "Atualizar canais",
"TaskUpdatePlugins": "Atualizar plugins",
"TaskCleanLogsDescription": "Elimina ficheiros de registo com mais de {0} dias.",
"TaskCleanLogs": "Limpar pasta de registos",
"TaskRefreshLibrary": "Analisar biblioteca multimédia",
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
"TasksChannelsCategory": "Canais de Internet",
"TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
"TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
"TaskCleanCacheDescription": "Elimina ficheiros de cache que já não são necessários ao sistema.",
"TasksChannelsCategory": "Canais da Internet",
"TaskRefreshChapterImages": "Extrair imagens dos capítulos",
"TaskDownloadMissingSubtitlesDescription": "Procura na Internet legendas em falta com base na configuração dos metadados.",
"TaskDownloadMissingSubtitles": "Transferir legendas em falta",
"TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
"TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
"TaskCleanTranscode": "Limpar o diretório de Transcode",
"TaskUpdatePluginsDescription": "Baixa e instala as atualizações para plug-ins configurados para atualização automática.",
"TaskRefreshPeopleDescription": "Atualizar metadados para elenco e equipa técnica da tua mediateca.",
"TaskRefreshChannelsDescription": "Atualiza as informações dos canais da Internet.",
"TaskCleanTranscodeDescription": "Elimina ficheiros de transcodificação com mais de um dia.",
"TaskCleanTranscode": "Limpar pasta de transcodificação",
"TaskUpdatePluginsDescription": "Transfere e instala atualizações dos plugins configurados para atualização automática.",
"TaskRefreshPeopleDescription": "Atualiza os metadados de atores e realizadores na tua biblioteca multimédia.",
"TaskRefreshPeople": "Atualizar pessoas",
"TaskRefreshLibraryDescription": "Analisar a mediateca para novos ficheiros e atualizar os metadados.",
"TaskCleanActivityLog": "Limpar registro de atividade",
"TaskRefreshLibraryDescription": "Analisa a biblioteca multimédia à procura de novos ficheiros e atualiza os metadados.",
"TaskCleanActivityLog": "Limpar registo de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
"Default": "Predefinição",
"TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.",
"TaskCleanActivityLogDescription": "Elimina as entradas do registo de atividade mais antigas do que o período configurado.",
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
"TaskOptimizeDatabaseDescription": "Compacta a base de dados e liberta espaço não utilizado. A execução desta tarefa depois de analisar a biblioteca ou de outras alterações que modifiquem a base de dados pode melhorar o desempenho.",
"External": "Externo",
"HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"HearingImpaired": "Deficiência auditiva",
"TaskKeyframeExtractor": "Extrator de fotogramas-chave",
"TaskKeyframeExtractorDescription": "Extrai fotogramas-chave de ficheiros de vídeo para criar playlists HLS mais precisas. Esta tarefa pode demorar algum tempo.",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
"TaskAudioNormalization": "Normalização de áudio",
"TaskDownloadMissingLyrics": "Transferir letra em falta",
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
"CleanupUserDataTask": "Task de limpeza de dados do usuário",
"CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de dias que não estão presentes há pelo menos 90 dias.",
"TaskDownloadMissingLyrics": "Transferir letras em falta",
"TaskDownloadMissingLyricsDescription": "Transfere letras de músicas",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de trickplay existentes de acordo com as definições da biblioteca.",
"TaskExtractMediaSegments": "Analisar segmentos multimédia",
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos multimédia de plugins com MediaSegment ativado.",
"TaskMoveTrickplayImages": "Migrar localização das imagens de trickplay",
"CleanupUserDataTask": "Limpeza de dados de utilizador",
"CleanupUserDataTaskDescription": "Remove todos os dados de utilizador (estado de reprodução, estado de favorito, etc.) de conteúdos multimédia que não estejam presentes há pelo menos 90 dias.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
"LyricDownloadFailureFromForItem": "Falha ao transferir letras de {0} para {1}",
"NameExtraBehindTheScenes": "Bastidores",
"NameExtraClip": "Excerto",
"NameExtraDeletedScene": "Cena eliminada",
"NameExtraFeaturette": "Minidocumentário",
"NameExtraInterview": "Entrevista",
"NameExtraSample": "Amostra",
"NameExtraShort": "Curta-metragem",
"NameExtraThemeSong": "Tema musical",
"NameExtraThemeVideo": "Vídeo temático",
"NameExtraScene": "Cena",
"NameExtraUnknown": "Extra",
"NameExtraTrailer": "Trailer",
"NameExtraNumbered": "{0} {1}"
}

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