Compare commits

...

104 Commits

Author SHA1 Message Date
Joshua M. Boniface
55a8d2555e Bump version to 10.7.5 2021-05-04 22:08:44 -04:00
Joshua M. Boniface
b7c3510da1 Revert "Merge pull request #5943 from Maxr1998/device-profile-defaults"
This PR broke direct play in JMP and caused aspect ratio issues in web.

This reverts commit 4c8df4c5bb.
2021-05-04 22:07:32 -04:00
Joshua M. Boniface
fd102abd81 Merge pull request #5973 from crobibero/legacy-ci-apiclient
Kill the CI

(cherry picked from commit d655145867)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-04 21:33:52 -04:00
Joshua M. Boniface
f8f7767cc5 Merge pull request #5968 from crobibero/legacy-ci-apiclient
(cherry picked from commit a598a8071b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-04 21:21:44 -04:00
Joshua M. Boniface
e8436814dc Bump version to 10.7.4 2021-05-04 21:21:44 -04:00
Joshua M. Boniface
14f63e8f2f Merge pull request #5970 from crobibero/fix-linux-test
Fix Linux Tests
2021-05-04 21:21:29 -04:00
crobibero
e764de0c80 Fix linux test for backport 2021-05-04 19:19:35 -06:00
Joshua M. Boniface
40147c9bb7 Merge pull request #5969 from crobibero/required-revert
Remove Required attributes

(cherry picked from commit fe0fce26e4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-04 21:14:59 -04:00
Joshua M. Boniface
3566d21ad1 Bump version to 10.7.3 2021-05-04 20:01:50 -04:00
Bond-009
4c8df4c5bb Merge pull request #5943 from Maxr1998/device-profile-defaults
(cherry picked from commit 9d3f614527)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-04 19:33:08 -04:00
Bond-009
9798bf29f3 Merge pull request #5937 from Maxr1998/videoscontroller-api-fix
Remove extraneous 'stream' parameter

(cherry picked from commit 266913c5d8)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-02 17:02:10 -04:00
Joshua M. Boniface
93ce087fc9 Merge pull request from GHSA-rgjw-4fwc-9v96
Remove /Images/Remote API endpoint

(cherry picked from commit e71cd8274a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-02 17:01:33 -04:00
Bond-009
e39495354b Merge pull request #5904 from cvium/fix-updatepeople-questionmark
add UpdatePeopleAsync and add people to both tables

(cherry picked from commit 5df87b3e0d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-29 14:56:58 -04:00
Bond-009
ee94fad8f7 Merge pull request #5903 from iwalton3/auto-leave-syncplay
Leave SyncPlay group on session disconnect.

(cherry picked from commit dcc2df75ec)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-29 14:56:58 -04:00
Claus Vium
53239b0529 Merge pull request #5861 from BaronGreenback/ProfileMatch
Change profile matching to match what the web interface says.

(cherry picked from commit 12496677bd)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-29 14:56:57 -04:00
Bond-009
cf0da1de86 Merge pull request #5826 from BaronGreenback/ssdpFix
PlayTo Fix: Use external ip not internal interface

(cherry picked from commit f4a59c92e6)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-29 14:56:30 -04:00
Bond-009
093510ae58 Merge pull request #5881 from cvium/tmdb-episode-externalids
Add tvrage and imdb ids for episodes

(cherry picked from commit e19d89bb4f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:25:41 -04:00
Bond-009
c3fafe9289 Merge pull request #5878 from Artiume/patch-2
(cherry picked from commit 95ab603a40)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:25:41 -04:00
Bond-009
bd914acd16 Merge pull request #5873 from cvium/fix-displaypref-migration
(cherry picked from commit 233900401e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:25:41 -04:00
Bond-009
81f9bec101 Merge pull request #5870 from cvium/fix-tmdbpersonprovider
(cherry picked from commit 6b103f7ab2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:25:40 -04:00
Bond-009
7db8601fbc Merge pull request #5863 from cvium/fix-index-migration
use IF NOT EXISTS in migration

(cherry picked from commit 5a4cfe11cf)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:25:40 -04:00
Bond-009
1ec247f5d8 Merge pull request #5860 from cvium/fix-notification-user-list
Fix notification disabled users list

(cherry picked from commit ebe8301404)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:25:39 -04:00
Bond-009
11e9173fbc Merge pull request #5859 from cvium/fix-streambuilder-permissions
Respect user settings for transcode and remux

(cherry picked from commit 5a6e60b414)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:24:27 -04:00
Bond-009
34508286a8 Merge pull request #5852 from cvium/fix-person-creation
Add Person to TypedBaseItems if it's new

(cherry picked from commit 4eeb69233d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:24:27 -04:00
Claus Vium
a82eded845 Merge pull request #5848 from sgmoore/IndexError
Fix ArgumentOutOfRangeException scanning AudioBooks

(cherry picked from commit 665220c4fb)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:24:27 -04:00
Bond-009
fcb729ff6b Merge pull request #5808 from cvium/semi-fix-collection-perf
(cherry picked from commit 48ed4b016c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-21 21:24:27 -04:00
Joshua M. Boniface
69f30bc52c Merge pull request #5782 from cvium/fix-release-10.7.2
Fix 10.7.2 nullable
2021-04-11 16:31:26 -04:00
cvium
3b605b6280 fix 10.7.2 nullable 2021-04-11 22:19:59 +02:00
Joshua M. Boniface
e8a359f97b Bump version 10.7.2 2021-04-11 14:19:21 -04:00
Bond-009
dbfaafc08a Merge pull request #5596 from BaronGreenback/DLNA_Hardening
Implemented DLNA exception handling

(cherry picked from commit 55102973d6)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:17:47 -04:00
Bond-009
de6747f6c5 Merge pull request #5385 from Bond-009/dlna2
Use XDocument.LoadAsync instead of XDocument.Parse

(cherry picked from commit a1cdc2c63f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:17:46 -04:00
Bond-009
100fe40b0a Merge pull request #5769 from cvium/workstation-gc
Enable Workstation GC mode

(cherry picked from commit f0625bb023)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Joshua M. Boniface
2197d20783 Merge pull request #5764 from cvium/fix-folders-perms
Do not check permissions for Folders collectiontype

(cherry picked from commit 770c123d12)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
f4f9ab777f Merge pull request #5750 from iwalton3/fix-audio-selection-uns
Fix setting audio stream in PlaybackInfo for jellyfin-web.

(cherry picked from commit bf510ca04e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Joshua M. Boniface
9ca7d62709 Merge pull request #5748 from cvium/playlist-audio-type
Set mediatype to Audio for playlists in a music library

(cherry picked from commit cd0daa7985)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
7aad16b6ec Merge pull request #5747 from cvium/more-convertimage-fixes
Catch IOException and include stack trace when saving images

(cherry picked from commit 240e67d485)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
f77673438e Merge pull request #5746 from cvium/dangling-symlinks
(cherry picked from commit 62117a6c12)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
bc2eb9fa79 Merge pull request #5736 from jellyfin/catch-httprequestex-librarymanager
fetching images should not kill the scanner

(cherry picked from commit 6577f1deb2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
df69ce55f7 Merge pull request #5734 from jellyfin/fix-isplayed-itemvalues
move IsPlayed to outerquery

(cherry picked from commit d532e95410)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
93cca4d50e Merge pull request #5725 from BrianCArnold/Fix2845_PlaylistDeletion
(cherry picked from commit 9d0467addf)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
3c64bcffe3 Merge pull request #5717 from cvium/nullable-custompref-value
(cherry picked from commit 47bbe4c146)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
6ece01d425 Merge pull request #5712 from BaronGreenback/5700
(cherry picked from commit 1a92d94e92)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
53f333bd64 Merge pull request #5702 from cvium/ws-auth
add simple auth handling to websocketmanager

(cherry picked from commit 3412120c61)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
9e459090ed Merge pull request #5693 from Maxr1998/probe-result-tweaks
(cherry picked from commit 7978f30ff7)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
95a4fc0f18 Merge pull request #5688 from crobibero/api-docs-sever-discovery
Add SessionDiscoveryInfo to generated api-docs

(cherry picked from commit f718735b4e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
62bf3db885 Merge pull request #5672 from jellyfin/skip-bad-images
ensure only valid images are saved in ItemImageProvider

(cherry picked from commit 38913a42b4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
42d702c091 Merge pull request #5671 from jellyfin/tmdbmovieprovider-originaltitle
set original title in tmdbmovieprovider

(cherry picked from commit 7c51d0a50e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
d07fe14814 Merge pull request #5661 from ferferga/openapi-product-version
Return Major.Minor.Build instead of Major.Minor.Build.Revision for OpenAPI

(cherry picked from commit cb111eb767)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
970eaf8dfb Merge pull request #5634 from cvium/directoryservice-case-sensitive
make directoryservice cache case sensitive

(cherry picked from commit 1de031a7c3)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
bc27c2b7da Merge pull request #5631 from BrianCArnold/FixMessageCommand
(cherry picked from commit a1718e392b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
37b969304a Merge pull request #5629 from lmaonator/fix-cast-stream-selection
(cherry picked from commit 90d9530aed)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
c6b5c4dda5 Merge pull request #5624 from crobibero/subtitle-format
(cherry picked from commit 9144d11a9d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
51f5da8015 Merge pull request #5621 from cvium/enable-range-processing-download
enable range processing for download endpoints

(cherry picked from commit 411570e6d4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
6e89ca9a34 Merge pull request #5620 from MrTimscampi/iso-ignore
(cherry picked from commit a76d997a86)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
de1896828f Merge pull request #5613 from accek/accek-samsung-dlna-fix
(cherry picked from commit e64f9f2f66)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
7d1d159b8a Merge pull request #5556 from oddstr13/image-fill-resize
(cherry picked from commit 790f7430aa)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
c3c98331d9 Merge pull request #5495 from BaronGreenback/RemoteAccessFix
(cherry picked from commit a890a85092)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Joshua M. Boniface
e78fa8c3ef Merge pull request #5416 from BaronGreenback/SubnetOverlappFix
(cherry picked from commit 19e7ebb279)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Bond-009
d63fb437c6 Merge pull request #5258 from Smith00101010/next-up-specials-fix
(cherry picked from commit 9d548c62ba)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-04-11 14:13:44 -04:00
Claus Vium
25c6388e23 Merge pull request #5600 from cvium/fix-hls-defaults-10.7
Fix hls defaults for 10.7
2021-03-28 00:15:57 +01:00
Claus Vium
8f16e10fc6 Apply suggestions from code review 2021-03-25 08:44:34 +01:00
cvium
1f07586d1c forgot streaminfo 2021-03-22 23:11:25 +01:00
cvium
5e0f480e48 fix build and isdirectstream 2021-03-22 23:08:09 +01:00
cvium
210d10400a change HLS endpoint defaults to false 2021-03-22 23:05:05 +01:00
Joshua M. Boniface
3dda25412c Bump version to 10.7.1 2021-03-21 19:18:08 -04:00
Joshua M. Boniface
0183ef8e89 Merge pull request from GHSA-wg4c-c9g9-rxhx
Fix issues 1 through 5 from GHSL-2021-050

(cherry picked from commit fe8cf29cad)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:13:08 -04:00
Claus Vium
75f39f0f2a Merge pull request #5559 from cvium/fix-tmdb-search-clean
Clean the entity name for non-words before searching

(cherry picked from commit 9360fecb31)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
966217e6a9 Merge pull request #5550 from cvium/revert_underscore_multiversion
(cherry picked from commit f42cee4790)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Joshua M. Boniface
328bcadabf Merge pull request #5532 from cvium/fix_episode_extras_questionmark
(cherry picked from commit 890a490776)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
0f38b2ffb2 Merge pull request #5518 from crobibero/missing-endpoints
Add missing InstantMix endpoints

(cherry picked from commit 8bb2420a25)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
40f4780825 Merge pull request #5515 from jellyfin/fix-refresh-endpoint
fix refresh endpoint

(cherry picked from commit 260b48ef9d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Claus Vium
546ffbe4f7 Merge pull request #5512 from crobibero/api-spec-version
(cherry picked from commit 94820f569b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Claus Vium
d00218c370 Merge pull request #5510 from BaronGreenback/DlnaFirstFix
Fix: Streaming crashing due to no deviceProfileId match.
(cherry picked from commit 109f24514f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
679d3f5873 Merge pull request #5504 from crobibero/json-string-converter
(cherry picked from commit 1a0ce16f4d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
787ad44323 Merge pull request #5500 from crobibero/api-integration-fix
Fix third party integration

(cherry picked from commit 7a988ef77d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
2ce6b347f5 Merge pull request #5480 from crobibero/api-session-message-type
Add SessionMessageType to generated openapi spec

(cherry picked from commit e3adc9ab74)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bill Thornton
318c1f7f0c Merge pull request #5476 from jellyfin/EraYaN-nuget-ci
Remove BuildPackage dependency for PublishNuget in CI

(cherry picked from commit 9fe3ca7a92)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Claus Vium
ed15cb1571 Merge pull request #5475 from BaronGreenback/SSDPFix
(cherry picked from commit baa43c6b41)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
c171bac71a Merge pull request #5461 from cvium/fix_multiversion
(cherry picked from commit d967267cef)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:09:59 -04:00
Bond-009
be5f511fc7 Merge pull request #5457 from cvium/fix_double_artist
(cherry picked from commit a037e30b41)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:27 -04:00
Claus Vium
a65c97c8f7 Merge pull request #5447 from joshuaboniface/fix-fedora-build
Remove Microsoft repo from install step

(cherry picked from commit 88a8fa7100)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:27 -04:00
Claus Vium
3fbe10364b Merge pull request #5444 from Ullmie02/hdhr-fix
(cherry picked from commit 329edd9dbe)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:27 -04:00
Claus Vium
88ab008112 Merge pull request #5431 from cvium/fix_tmdb_imdbid
Use imdbid as fallback in movie provider

(cherry picked from commit 84e16a8535)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:27 -04:00
Bond-009
1518f6d325 Merge pull request #5428 from cvium/fix_tmdb_year
Default to the searchinfo year, fallback to parsed year

(cherry picked from commit 97fd136a8c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:26 -04:00
Claus Vium
53576fe1b8 Merge pull request #5403 from BaronGreenback/DLNAProfileFix
(cherry picked from commit 5592967497)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:26 -04:00
Claus Vium
da3b7bb684 Merge pull request #5324 from danieladov/master
Fix duplicated movies when group movies into collections is enabled

(cherry picked from commit bd70f56218)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:26 -04:00
Joshua M. Boniface
4a320b26b5 Bump version to 10.7.0
Also clear out old changelogs since these don't actually make sense
anyways.
2021-03-08 18:11:29 -05:00
Joshua M. Boniface
63868eca40 Merge pull request #5409 from ikomhoog/master
(cherry picked from commit 82d88bdec6)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:26 -05:00
Claus Vium
f6e8493d69 Merge pull request #5407 from Bond-009/hack
(cherry picked from commit 90cdd1345d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:26 -05:00
Joshua M. Boniface
3c3b536e81 Merge pull request #5406 from cvium/trycleanstring-dont-die-on-me
(cherry picked from commit 0ef8bea125)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:26 -05:00
Joshua M. Boniface
a10eea41ac Merge pull request #5402 from Ullmie02/fix-null-size
Use FileShare.None when creating files

(cherry picked from commit 480dd66428)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:25 -05:00
Bond-009
42d0c1ac5f Merge pull request #5381 from cvium/fix-network-substitution
(cherry picked from commit 497ea57fd2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:25 -05:00
Joshua M. Boniface
b01290013e Merge pull request #5315 from BaronGreenback/FixFor5280Part2
(cherry picked from commit 3c46f10e3d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:20 -05:00
Bond-009
132335a747 Merge pull request #5383 from cvium/fix-mergeversions-overflow
do not pick a linked item as primary when merging versions

(cherry picked from commit 3741be51ec)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:30:11 -05:00
Claus Vium
75d3d120d3 Fix UpdateMediaPath model binding (#5378)
(cherry picked from commit d0a2d00b29)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:28:08 -05:00
Bond-009
e8890cc682 Merge pull request #5377 from cvium/fix-tmdb-image-languages
Do not use language or imagelanguages when searching for images with TMDb

(cherry picked from commit 1d87274cc2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:27:58 -05:00
Bond-009
e4bf57c739 Merge pull request #5375 from crobibero/default-api-value
Specify defaults or set query parameter to nullable

(cherry picked from commit a0f6bc14a2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:27:58 -05:00
Claus Vium
046dd7fa60 Merge pull request #5356 from cvium/fix_provideridextensions
return false when providerid is null or empty

(cherry picked from commit ddc62a89ba)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:27:58 -05:00
David Ullmer
5e18ab3604 Fix TMDb search name containing year (#5349)
(cherry picked from commit 8f99bdd07c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:54 -05:00
dkanada
7545b1286b Merge pull request #5345 from BaronGreenback/IP6Fix
Dual IP4 / IP6 server fails on non-windows platforms

(cherry picked from commit 8615847a8a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:41 -05:00
Bond-009
b99db64f8f Merge pull request #5342 from BaronGreenback/errorMessageCorrection
Corrected logging message

(cherry picked from commit 1f0bbe266c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:41 -05:00
Claus Vium
20810eedbe Merge pull request #5339 from Bond-009/hasproviderids
Revert breaking change to HasProviderId

(cherry picked from commit e858e5f0b8)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:41 -05:00
BaronGreenback
2d88b8346d Remove Content-Length header from DLNA HEAD request (#5335)
(cherry picked from commit d819a1d928)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:38 -05:00
Bond-009
eafaccae5d Merge pull request #5004 from jellyfin/camera-upload
remove unused notification type

(cherry picked from commit bffebce909)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:24:50 -05:00
164 changed files with 3035 additions and 1398 deletions

View File

@@ -1,59 +0,0 @@
parameters:
- name: LinuxImage
type: string
default: "ubuntu-latest"
- name: GeneratorVersion
type: string
default: "5.0.1"
jobs:
- job: GenerateApiClients
displayName: 'Generate Api Clients'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
dependsOn: Test
pool:
vmImage: "${{ parameters.LinuxImage }}"
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec Artifact'
inputs:
source: 'current'
artifact: "OpenAPI Spec"
path: "$(System.ArtifactsDirectory)/openapispec"
runVersion: "latest"
- task: CmdLine@2
displayName: 'Download OpenApi Generator'
inputs:
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
## Authenticate with npm registry
- task: npmAuthenticate@0
inputs:
workingFile: ./.npmrc
customEndpoint: 'jellyfin-bot for NPM'
## Generate npm api client
- task: CmdLine@2
displayName: 'Build stable typescript axios client'
inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
## Run npm install
- task: Npm@1
displayName: 'Install npm dependencies'
inputs:
command: install
workingDir: ./apiclient/generated/typescript/axios
## Publish npm packages
- task: Npm@1
displayName: 'Publish stable typescript axios client'
inputs:
command: custom
customCommand: publish --access public
publishRegistry: useExternalRegistry
publishEndpoint: 'jellyfin-bot for NPM'
workingDir: ./apiclient/generated/typescript/axios

View File

@@ -160,7 +160,6 @@ jobs:
dependsOn:
- BuildPackage
- BuildDocker
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
@@ -186,9 +185,6 @@ jobs:
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: succeeded('BuildPackage')
pool:
vmImage: 'ubuntu-latest'

View File

@@ -61,6 +61,3 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-api-client.yml

View File

@@ -49,6 +49,7 @@
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
@@ -103,6 +104,7 @@
- [shemanaev](https://github.com/shemanaev)
- [skaro13](https://github.com/skaro13)
- [sl1288](https://github.com/sl1288)
- [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)

View File

@@ -111,7 +111,7 @@ namespace Emby.Dlna
if (profile != null)
{
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
}
else
{
@@ -138,80 +138,45 @@ namespace Emby.Dlna
_logger.LogInformation(builder.ToString());
}
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
/// <summary>
/// Attempts to match a device with a profile.
/// Rules:
/// - If the profile field has no value, the field matches irregardless of its contents.
/// - the profile field can be an exact match, or a reg exp.
/// </summary>
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
/// <returns><b>True</b> if they match.</returns>
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
{
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
{
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
{
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
{
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
{
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelName))
{
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
{
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
{
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
{
return false;
}
}
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
{
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
{
return false;
}
}
return true;
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
}
private bool IsRegexOrSubstringMatch(string input, string pattern)
{
if (string.IsNullOrEmpty(pattern))
{
// In profile identification: An empty pattern matches anything.
return true;
}
if (string.IsNullOrEmpty(input))
{
// The profile contains a value, and the device doesn't.
return false;
}
try
{
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
catch (ArgumentException ex)
{
@@ -333,7 +298,12 @@ namespace Emby.Dlna
throw new ArgumentNullException(nameof(id));
}
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
if (info == null)
{
return null;
}
return ParseProfileFile(info.Path, info.Info.Type);
}
@@ -395,7 +365,8 @@ namespace Emby.Dlna
{
Directory.CreateDirectory(systemProfilesPath);
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}

View File

@@ -128,7 +128,8 @@ namespace Emby.Dlna.Main
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
if (_disabled)
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
@@ -316,7 +317,7 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
if (_appHost.PublishedServerUrl == null)
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
{
// DLNA will only work over http, so we must reset to http:// : {port}.
uri.Scheme = "http";

View File

@@ -219,7 +219,7 @@ namespace Emby.Dlna.PlayTo
{
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
if (command == null)
{
return false;
@@ -253,7 +253,7 @@ namespace Emby.Dlna.PlayTo
{
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
if (command == null)
{
return;
@@ -278,7 +278,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
if (command == null)
{
return;
@@ -305,7 +305,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
if (command == null)
{
return;
@@ -378,6 +378,10 @@ namespace Emby.Dlna.PlayTo
public async Task SetPlay(CancellationToken cancellationToken)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
if (avCommands == null)
{
return;
}
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
@@ -388,7 +392,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
if (command == null)
{
return;
@@ -406,7 +410,7 @@ namespace Emby.Dlna.PlayTo
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
if (command == null)
{
return;
@@ -528,7 +532,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
if (command == null)
{
return;
@@ -578,7 +582,7 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
if (command == null)
{
return;
@@ -665,6 +669,10 @@ namespace Emby.Dlna.PlayTo
}
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
{
return null;
}
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
@@ -733,6 +741,11 @@ namespace Emby.Dlna.PlayTo
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
if (rendererCommands == null)
{
return (false, null);
}
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
@@ -914,6 +927,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
AvCommands = TransportCommands.Create(document);
return AvCommands;
@@ -942,6 +959,10 @@ namespace Emby.Dlna.PlayTo
var httpClient = new SsdpHttpClient(_httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
RendererCommands = TransportCommands.Create(document);
return RendererCommands;
@@ -973,6 +994,10 @@ namespace Emby.Dlna.PlayTo
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
if (document == null)
{
return null;
}
var friendlyNames = new List<string>();

View File

@@ -132,7 +132,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{
if (_disposed)
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
return;
}
@@ -943,11 +943,7 @@ namespace Emby.Dlna.PlayTo
request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
// Be careful, IsDirectStream==true by default (Static != false or not in query).
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");

View File

@@ -178,12 +178,17 @@ namespace Emby.Dlna.PlayTo
if (controller == null)
{
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
if (device == null)
{
_logger.LogError("Ignoring device as xml response is invalid.");
return;
}
string deviceName = device.Properties.Name;
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
controller = new PlayToController(
sessionInfo,

View File

@@ -45,10 +45,10 @@ namespace Emby.Dlna.PlayTo
cancellationToken)
.ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
LoadOptions.PreserveWhitespace);
return await XDocument.LoadAsync(
stream,
LoadOptions.PreserveWhitespace,
cancellationToken).ConfigureAwait(false);
}
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -94,10 +94,17 @@ namespace Emby.Dlna.PlayTo
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream, Encoding.UTF8);
return XDocument.Parse(
await reader.ReadToEndAsync().ConfigureAwait(false),
LoadOptions.PreserveWhitespace);
try
{
return await XDocument.LoadAsync(
stream,
LoadOptions.PreserveWhitespace,
cancellationToken).ConfigureAwait(false);
}
catch
{
return null;
}
}
private async Task<HttpResponseMessage> PostSoapDataAsync(

View File

@@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
public class TransportCommands
{
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
private List<StateVariable> _stateVariables = new List<StateVariable>();
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
public List<StateVariable> StateVariables => _stateVariables;
public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
public List<ServiceAction> ServiceActions => _serviceActions;
public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
public static TransportCommands Create(XDocument document)
{

View File

@@ -1,5 +1,7 @@
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.Linq;
using MediaBrowser.Model.Dlna;
@@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
{
public DefaultProfile()
{
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
Name = "Generic Device";
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";

View File

@@ -69,7 +69,7 @@ namespace Emby.Dlna.Ssdp
{
lock (_syncLock)
{
if (_listenerCount > 0 && _deviceLocator == null)
if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
{
_deviceLocator = new SsdpDeviceLocator(_commsServer);
@@ -104,7 +104,7 @@ namespace Emby.Dlna.Ssdp
{
Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers,
LocalIpAddress = e.LocalIpAddress
RemoteIpAddress = e.RemoteIpAddress
});
DeviceDiscoveredInternal?.Invoke(this, args);

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
@@ -171,11 +172,26 @@ namespace Emby.Drawing
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
string cacheFilePath = GetCacheFilePath(
originalImagePath,
options.Width,
options.Height,
options.MaxWidth,
options.MaxHeight,
options.FillWidth,
options.FillHeight,
quality,
dateModified,
outputFormat,
options.AddPlayedIndicator,
options.PercentPlayed,
options.UnplayedCount,
options.Blur,
options.BackgroundColor,
options.ForegroundLayer);
try
{
@@ -246,48 +262,111 @@ namespace Emby.Drawing
/// <summary>
/// Gets the cache file path based on a set of parameters.
/// </summary>
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
private string GetCacheFilePath(
string originalPath,
int? width,
int? height,
int? maxWidth,
int? maxHeight,
int? fillWidth,
int? fillHeight,
int quality,
DateTime dateModified,
ImageFormat format,
bool addPlayedIndicator,
double percentPlayed,
int? unwatchedCount,
int? blur,
string backgroundColor,
string foregroundLayer)
{
var filename = originalPath
+ "width=" + outputSize.Width
+ "height=" + outputSize.Height
+ "quality=" + quality
+ "datemodified=" + dateModified.Ticks
+ "f=" + format;
var filename = new StringBuilder(256);
filename.Append(originalPath);
filename.Append(",quality=");
filename.Append(quality);
filename.Append(",datemodified=");
filename.Append(dateModified.Ticks);
filename.Append(",f=");
filename.Append(format);
if (width.HasValue)
{
filename.Append(",width=");
filename.Append(width.Value);
}
if (height.HasValue)
{
filename.Append(",height=");
filename.Append(height.Value);
}
if (maxWidth.HasValue)
{
filename.Append(",maxwidth=");
filename.Append(maxWidth.Value);
}
if (maxHeight.HasValue)
{
filename.Append(",maxheight=");
filename.Append(maxHeight.Value);
}
if (fillWidth.HasValue)
{
filename.Append(",fillwidth=");
filename.Append(fillWidth.Value);
}
if (fillHeight.HasValue)
{
filename.Append(",fillheight=");
filename.Append(fillHeight.Value);
}
if (addPlayedIndicator)
{
filename += "pl=true";
filename.Append(",pl=true");
}
if (percentPlayed > 0)
{
filename += "p=" + percentPlayed;
filename.Append(",p=");
filename.Append(percentPlayed);
}
if (unwatchedCount.HasValue)
{
filename += "p=" + unwatchedCount.Value;
filename.Append(",p=");
filename.Append(unwatchedCount.Value);
}
if (blur.HasValue)
{
filename += "blur=" + blur.Value;
filename.Append(",blur=");
filename.Append(blur.Value);
}
if (!string.IsNullOrEmpty(backgroundColor))
{
filename += "b=" + backgroundColor;
filename.Append(",b=");
filename.Append(backgroundColor);
}
if (!string.IsNullOrEmpty(foregroundLayer))
{
filename += "fl=" + foregroundLayer;
filename.Append(",fl=");
filename.Append(foregroundLayer);
}
filename += "v=" + Version;
filename.Append(",v=");
filename.Append(Version);
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant());
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
}
/// <inheritdoc />

View File

@@ -33,7 +33,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.7.0</VersionPrefix>
<VersionPrefix>10.7.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -33,6 +33,12 @@ namespace Emby.Naming.Video
private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
{
if (string.IsNullOrEmpty(name))
{
newName = ReadOnlySpan<char>.Empty;
return false;
}
var match = expression.Match(name);
int index = match.Index;
if (match.Success && index != 0)
@@ -41,7 +47,7 @@ namespace Emby.Naming.Video
return true;
}
newName = string.Empty;
newName = ReadOnlySpan<char>.Empty;
return false;
}
}

View File

@@ -222,20 +222,21 @@ namespace Emby.Naming.Video
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.ToString();
}
// Remove the folder name before cleaning as we don't care about cleaning that part
if (folderName.Length <= testFilename.Length)
{
testFilename = testFilename.Substring(folderName.Length).Trim();
}
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.Trim().ToString();
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(testFilename)
|| testFilename[0].Equals('-')
|| testFilename[0].Equals('_')
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
|| testFilename[0] == '-'
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
}
return false;

View File

@@ -75,10 +75,6 @@ namespace Emby.Notifications
Type = NotificationType.VideoPlaybackStopped.ToString()
},
new NotificationTypeInfo
{
Type = NotificationType.CameraImageUploaded.ToString()
},
new NotificationTypeInfo
{
Type = NotificationType.UserLockedOut.ToString()
},
@@ -114,10 +110,6 @@ namespace Emby.Notifications
{
note.Category = _localization.GetLocalizedString("Plugin");
}
else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("Sync");
}
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("User");

View File

@@ -53,7 +53,8 @@ namespace Emby.Server.Implementations.AppBase
Directory.CreateDirectory(directory);
// Save it after load in case we got new items
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
fs.Write(newBytes, 0, newBytesLen);
}

View File

@@ -43,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Networking.Configuration;
@@ -98,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
@@ -117,6 +119,7 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
@@ -134,9 +137,6 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; }
/// <inheritdoc />
public Uri PublishedServerUrl => _startupOptions.PublishedServerUrl;
public virtual bool CanLaunchWebBrowser
{
get
@@ -230,6 +230,11 @@ namespace Emby.Server.Implementations
/// </summary>
public int HttpsPort { get; private set; }
/// <summary>
/// Gets the value of the PublishedServerUrl setting.
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <summary>
/// Gets the server configuration manager.
/// </summary>
@@ -242,12 +247,14 @@ namespace Emby.Server.Implementations
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection serviceCollection)
{
@@ -271,6 +278,7 @@ namespace Emby.Server.Implementations
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_startupOptions = options;
_startupConfig = startupConfig;
// Initialize runtime stat collection
if (ServerConfigurationManager.Configuration.EnableMetrics)
@@ -1151,10 +1159,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{
// Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(ipAddress, out port);
@@ -1171,10 +1179,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(HttpRequest request, int? port = null)
{
// Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(request, out port);
@@ -1191,10 +1199,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(string hostname, int? port = null)
{
// Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(hostname, out port);

View File

@@ -124,7 +124,7 @@ namespace Emby.Server.Implementations.Collections
private IEnumerable<BoxSet> GetCollections(User user)
{
var folder = GetCollectionsFolder(false).Result;
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
return folder == null
? Enumerable.Empty<BoxSet>()
@@ -319,11 +319,11 @@ namespace Emby.Server.Implementations.Collections
{
var results = new Dictionary<Guid, BaseItem>();
var allBoxsets = GetCollections(user).ToList();
var allBoxSets = GetCollections(user).ToList();
foreach (var item in items)
{
if (!(item is ISupportsBoxSetGrouping))
if (item is not ISupportsBoxSetGrouping)
{
results[item.Id] = item;
}
@@ -331,20 +331,44 @@ namespace Emby.Server.Implementations.Collections
{
var itemId = item.Id;
var currentBoxSets = allBoxsets
.Where(i => i.ContainsLinkedChildByItemId(itemId))
.ToList();
if (currentBoxSets.Count > 0)
var itemIsInBoxSet = false;
foreach (var boxSet in allBoxSets)
{
foreach (var boxset in currentBoxSets)
if (!boxSet.ContainsLinkedChildByItemId(itemId))
{
results[boxset.Id] = boxset;
continue;
}
itemIsInBoxSet = true;
results.TryAdd(boxSet.Id, boxSet);
}
// skip any item that is in a box set
if (itemIsInBoxSet)
{
continue;
}
var alreadyInResults = false;
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
if (item is Video video)
{
foreach (var childId in video.GetLocalAlternateVersionIds())
{
if (!results.ContainsKey(childId))
{
continue;
}
alreadyInResults = true;
break;
}
}
else
if (!alreadyInResults)
{
results[item.Id] = item;
results[itemId] = item;
}
}
}

View File

@@ -5415,7 +5415,6 @@ AND Type = @InternalPersonType)");
ItemIds = query.ItemIds,
TopParentIds = query.TopParentIds,
ParentId = query.ParentId,
IsPlayed = query.IsPlayed,
IsAiring = query.IsAiring,
IsMovie = query.IsMovie,
IsSports = query.IsSports,
@@ -5441,6 +5440,7 @@ AND Type = @InternalPersonType)");
var outerQuery = new InternalItemsQuery(query.User)
{
IsPlayed = query.IsPlayed,
IsFavorite = query.IsFavorite,
IsFavoriteOrLiked = query.IsFavoriteOrLiked,
IsLiked = query.IsLiked,

View File

@@ -14,15 +14,18 @@ namespace Emby.Server.Implementations.HttpServer
public class WebSocketManager : IWebSocketManager
{
private readonly IWebSocketListener[] _webSocketListeners;
private readonly IAuthService _authService;
private readonly ILogger<WebSocketManager> _logger;
private readonly ILoggerFactory _loggerFactory;
public WebSocketManager(
IAuthService authService,
IEnumerable<IWebSocketListener> webSocketListeners,
ILogger<WebSocketManager> logger,
ILoggerFactory loggerFactory)
{
_webSocketListeners = webSocketListeners.ToArray();
_authService = authService;
_logger = logger;
_loggerFactory = loggerFactory;
}
@@ -30,6 +33,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context)
{
_ = _authService.Authenticate(context.Request);
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);

View File

@@ -249,9 +249,18 @@ namespace Emby.Server.Implementations.IO
// Issue #2354 get the size of files behind symbolic links
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
try
{
result.Length = thisFileStream.Length;
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
{
result.Length = thisFileStream.Length;
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
}

View File

@@ -1,5 +1,5 @@
#pragma warning disable CS1591
#nullable enable
using System;
namespace Emby.Server.Implementations
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the value of the --ffmpeg command line option.
/// </summary>
string FFmpegPath { get; }
string? FFmpegPath { get; }
/// <summary>
/// Gets the value of the --service command line option.
@@ -19,21 +19,21 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the value of the --package-name command line option.
/// </summary>
string PackageName { get; }
string? PackageName { get; }
/// <summary>
/// Gets the value of the --restartpath command line option.
/// </summary>
string RestartPath { get; }
string? RestartPath { get; }
/// <summary>
/// Gets the value of the --restartargs command line option.
/// </summary>
string RestartArgs { get; }
string? RestartArgs { get; }
/// <summary>
/// Gets the value of the --published-server-url command line option.
/// </summary>
Uri PublishedServerUrl { get; }
string? PublishedServerUrl { get; }
}
}

View File

@@ -1247,7 +1247,7 @@ namespace Emby.Server.Implementations.Library
{
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
// https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse<CollectionTypeOptions>(Path.GetExtension(file), true, out var res))
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
{
return res;
}
@@ -1914,12 +1914,17 @@ namespace Emby.Server.Implementations.Library
}
catch (ArgumentException)
{
_logger.LogWarning("Cannot get image index for {0}", img.Path);
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
continue;
}
catch (InvalidOperationException)
catch (Exception ex) when (ex is InvalidOperationException || ex is IOException)
{
_logger.LogWarning("Cannot fetch image from {0}", img.Path);
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
continue;
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", img.Path, ex.StatusCode);
continue;
}
}
@@ -1932,7 +1937,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path);
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
image.Width = 0;
image.Height = 0;
continue;
@@ -1944,7 +1949,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path);
_logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
image.BlurHash = string.Empty;
}
@@ -1954,7 +1959,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot update DateModified for {0}", image.Path);
_logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
}
}
@@ -2776,6 +2781,7 @@ namespace Emby.Server.Implementations.Library
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
{
string newPath;
if (ownerItem != null)
{
var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2783,15 +2789,9 @@ namespace Emby.Server.Implementations.Library
{
foreach (var pathInfo in libraryOptions.PathInfos)
{
if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath))
{
continue;
}
var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
return newPath;
}
}
}
@@ -2800,24 +2800,16 @@ namespace Emby.Server.Implementations.Library
var metadataPath = _configurationManager.Configuration.MetadataPath;
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath))
{
var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
if (metadataSubstitutionResult.Item2)
{
return metadataSubstitutionResult.Item1;
}
return newPath;
}
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
{
if (!string.IsNullOrWhiteSpace(map.From))
if (path.TryReplaceSubPath(map.From, map.To, out newPath))
{
var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
}
return newPath;
}
}
@@ -2826,47 +2818,12 @@ namespace Emby.Server.Implementations.Library
public string SubstitutePath(string path, string from, string to)
{
return SubstitutePathInternal(path, from, to).Item1;
}
private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
{
if (string.IsNullOrWhiteSpace(path))
if (path.TryReplaceSubPath(from, to, out var newPath))
{
throw new ArgumentNullException(nameof(path));
return newPath;
}
if (string.IsNullOrWhiteSpace(from))
{
throw new ArgumentNullException(nameof(from));
}
if (string.IsNullOrWhiteSpace(to))
{
throw new ArgumentNullException(nameof(to));
}
from = from.Trim();
to = to.Trim();
var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
var changed = false;
if (!string.Equals(newPath, path, StringComparison.Ordinal))
{
if (to.IndexOf('/', StringComparison.Ordinal) != -1)
{
newPath = newPath.Replace('\\', '/');
}
else
{
newPath = newPath.Replace('/', '\\');
}
changed = true;
}
return new Tuple<string, bool>(newPath, changed);
return path;
}
private void SetExtraTypeFromFilename(Video item)
@@ -2923,6 +2880,12 @@ namespace Emby.Server.Implementations.Library
}
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
{
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
}
/// <inheritdoc />
public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
{
if (!item.SupportsPeople)
{
@@ -2930,6 +2893,8 @@ namespace Emby.Server.Implementations.Library
}
_itemRepository.UpdatePeople(item.Id, people);
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
@@ -3001,7 +2966,7 @@ namespace Emby.Server.Implementations.Library
if (collectionType != null)
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString() + ".collection");
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty<byte>());
}
@@ -3033,6 +2998,58 @@ namespace Emby.Server.Implementations.Library
}
}
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
var personsToSave = new List<BaseItem>();
foreach (var person in people)
{
cancellationToken.ThrowIfCancellationRequested();
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
var personEntity = GetPerson(person.Name);
// if PresentationUniqueKey is empty it's likely a new item.
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
{
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
}
foreach (var id in person.ProviderIds)
{
if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase))
{
personEntity.SetProviderId(id.Key, id.Value);
saveEntity = true;
}
}
if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
{
personEntity.SetImage(
new ItemImageInfo
{
Path = person.ImageUrl,
Type = ImageType.Primary
},
0);
saveEntity = true;
itemUpdateType = ItemUpdateType.ImageUpdate;
}
if (saveEntity)
{
personsToSave.Add(personEntity);
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
}
}
CreateItems(personsToSave, null, CancellationToken.None);
}
private void StartScanInBackground()
{
Task.Run(() =>

View File

@@ -200,10 +200,15 @@ namespace Emby.Server.Implementations.Library
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
}
}
}
return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList();
return SortMediaSources(list);
}
public MediaProtocol GetPathProtocol(string path)
@@ -437,7 +442,7 @@ namespace Emby.Server.Implementations.Library
}
}
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
{
@@ -452,8 +457,9 @@ namespace Emby.Server.Implementations.Library
{
var stream = i.VideoStream;
return stream == null || stream.Width == null ? 0 : stream.Width.Value;
return stream?.Width ?? 0;
})
.Where(i => i.Type != MediaSourceType.Placeholder)
.ToList();
}

View File

@@ -1,6 +1,8 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.RegularExpressions;
namespace Emby.Server.Implementations.Library
@@ -47,5 +49,59 @@ namespace Emby.Server.Implementations.Library
return null;
}
/// <summary>
/// Replaces a sub path with another sub path and normalizes the final path.
/// </summary>
/// <param name="path">The original path.</param>
/// <param name="subPath">The original sub path.</param>
/// <param name="newSubPath">The new sub path.</param>
/// <param name="newPath">The result of the sub path replacement</param>
/// <returns>The path after replacing the sub path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
public static bool TryReplaceSubPath(this string path, string subPath, string newSubPath, [NotNullWhen(true)] out string? newPath)
{
newPath = null;
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(subPath) || string.IsNullOrEmpty(newSubPath) || subPath.Length > path.Length)
{
return false;
}
char oldDirectorySeparatorChar;
char newDirectorySeparatorChar;
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
// The reasoning behind this is that a forward slash likely means it's a Linux path and
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
if (newSubPath.Contains('/', StringComparison.Ordinal))
{
oldDirectorySeparatorChar = '\\';
newDirectorySeparatorChar = '/';
}
else
{
oldDirectorySeparatorChar = '/';
newDirectorySeparatorChar = '\\';
}
path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
// when the sub path matches a similar but in-complete subpath
var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase)
|| (!oldSubPathEndsWithSeparator && path[subPath.Length] != newDirectorySeparatorChar))
{
return false;
}
var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar);
// Ensure that the path with the old subpath removed starts with a leading dir separator
int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length;
newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx));
return true;
}
}
}

View File

@@ -201,6 +201,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue;
}
if (resolvedItem.Files.Count == 0)
{
continue;
}
var firstMedia = resolvedItem.Files[0];
var libraryItem = new T

View File

@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
protected override T Resolve(ItemResolveArgs args)
public override T Resolve(ItemResolveArgs args)
{
return ResolveVideo<T>(args, false);
}
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <param name="args">The args.</param>
/// <param name="parseName">if set to <c>true</c> [parse name].</param>
/// <returns>``0.</returns>
protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();

View File

@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
protected override Book Resolve(ItemResolveArgs args)
public override Book Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();

View File

@@ -69,6 +69,110 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result;
}
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Video.</returns>
public override Video Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
// Find movies with their own folders
if (args.IsDirectory)
{
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
var files = args.FileSystemChildren
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
.ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
{
// Owned items will be caught by the plain video resolver
if (args.Parent == null)
{
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
return null;
}
if (args.HasParent<Series>())
{
return null;
}
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
return null;
}
// Handle owned items
if (args.Parent == null)
{
return base.Resolve(args);
}
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
Video item = null;
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Movie>(args, true);
}
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Video>(args, false);
}
else if (string.IsNullOrEmpty(collectionType))
{
if (args.HasParent<Series>())
{
return null;
}
item = ResolveVideo<Video>(args, false);
}
if (item != null)
{
item.IsInMixedFolder = true;
}
return item;
}
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
@@ -216,110 +320,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Video.</returns>
protected override Video Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
// Find movies with their own folders
if (args.IsDirectory)
{
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
var files = args.FileSystemChildren
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
.ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
{
// Owned items will be caught by the plain video resolver
if (args.Parent == null)
{
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
return null;
}
if (args.HasParent<Series>())
{
return null;
}
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
return null;
}
// Handle owned items
if (args.Parent == null)
{
return base.Resolve(args);
}
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
Video item = null;
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Movie>(args, true);
}
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Video>(args, false);
}
else if (string.IsNullOrEmpty(collectionType))
{
if (args.HasParent<Series>())
{
return null;
}
item = ResolveVideo<Video>(args, false);
}
if (item != null)
{
item.IsInMixedFolder = true;
}
return item;
}
/// <summary>
/// Sets the initial item values.
/// </summary>

View File

@@ -63,7 +63,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
Path = args.Path,
Name = Path.GetFileNameWithoutExtension(args.Path),
IsInMixedFolder = true
IsInMixedFolder = true,
PlaylistMediaType = MediaType.Audio
};
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -11,12 +12,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
public class EpisodeResolver : BaseVideoResolver<Episode>
{
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public EpisodeResolver(ILibraryManager libraryManager)
: base(libraryManager)
{
}
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Episode.</returns>
protected override Episode Resolve(ItemResolveArgs args)
public override Episode Resolve(ItemResolveArgs args)
{
var parent = args.Parent;
@@ -34,11 +44,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
season = parent.GetParents().OfType<Season>().FirstOrDefault();
}
// If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
if (season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
if ((season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
{
var episode = ResolveVideo<Episode>(args, false);
@@ -74,14 +85,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public EpisodeResolver(ILibraryManager libraryManager)
: base(libraryManager)
{
}
}
}

View File

@@ -248,15 +248,15 @@ namespace Emby.Server.Implementations.Library
}
else if (positionTicks > 0 && hasRuntime && item is AudioBook)
{
var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes;
var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
if (minIn > _config.Configuration.MinAudiobookResume)
if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
{
// ignore progress during the beginning
positionTicks = 0;
}
else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
{
// mark as completed close to the end
positionTicks = 0;

View File

@@ -45,7 +45,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
onStarted();
@@ -70,7 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
onStarted();

View File

@@ -1860,7 +1860,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -1924,7 +1925,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{

View File

@@ -82,11 +82,6 @@ namespace Emby.Server.Implementations.MediaEncoder
return false;
}
if (video.VideoType == VideoType.Dvd)
{
return false;
}
if (video.IsShortcut)
{
return false;

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
@@ -368,7 +369,7 @@ namespace Emby.Server.Implementations.Plugins
}
/// <inheritdoc/>
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path)
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
{
if (packageInfo == null)
{
@@ -411,9 +412,9 @@ namespace Emby.Server.Implementations.Plugins
Overview = packageInfo.Overview,
Owner = packageInfo.Owner,
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp),
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
Version = versionInfo.Version,
Status = PluginStatus.Active,
Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
AutoUpdate = true,
ImagePath = imagePath
};

View File

@@ -131,11 +131,11 @@ namespace Emby.Server.Implementations.Sorting
return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y));
}
private static int GetSpecialCompareValue(Episode item)
private static long GetSpecialCompareValue(Episode item)
{
// First sort by season number
// Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough)
var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000;
var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000L;
// Second sort order is if it airs after the season
if (item.AirsAfterSeasonNumber.HasValue)

View File

@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
_sessionManager = sessionManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
_sessionManager.SessionControllerConnected += OnSessionControllerConnected;
_sessionManager.SessionEnded += OnSessionEnded;
}
/// <inheritdoc />
@@ -352,18 +352,18 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
_sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
_sessionManager.SessionEnded -= OnSessionEnded;
_disposed = true;
}
private void OnSessionControllerConnected(object sender, SessionEventArgs e)
private void OnSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
{
var request = new JoinGroupRequest(group.GroupId);
JoinGroup(session, request, CancellationToken.None);
var leaveGroupRequest = new LeaveGroupRequest();
LeaveGroup(session, leaveGroupRequest, CancellationToken.None);
}
}

View File

@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -11,7 +10,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Series = MediaBrowser.Controller.Entities.TV.Series;
@@ -23,12 +21,14 @@ namespace Emby.Server.Implementations.TV
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager)
public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager)
{
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_configurationManager = configurationManager;
}
public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
@@ -200,13 +200,10 @@ namespace Emby.Server.Implementations.TV
ParentIndexNumberNotEquals = 0,
DtoOptions = new DtoOptions
{
Fields = new ItemFields[]
{
ItemFields.SortName
},
Fields = new[] { ItemFields.SortName },
EnableImages = false
}
}).FirstOrDefault();
}).Cast<Episode>().FirstOrDefault();
Func<Episode> getEpisode = () =>
{
@@ -224,6 +221,43 @@ namespace Emby.Server.Implementations.TV
DtoOptions = dtoOptions
}).Cast<Episode>().FirstOrDefault();
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
{
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
IncludeItemTypes = new[] { nameof(Episode) },
IsPlayed = false,
IsVirtualItem = false,
DtoOptions = dtoOptions
})
.Cast<Episode>()
.Where(episode => episode.AirsBeforeSeasonNumber != null || episode.AirsAfterSeasonNumber != null)
.ToList();
if (lastWatchedEpisode != null)
{
// Last watched episode is added, because there could be specials that aired before the last watched episode
consideredEpisodes.Add(lastWatchedEpisode);
}
if (nextEpisode != null)
{
consideredEpisodes.Add(nextEpisode);
}
var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
.Cast<Episode>();
if (lastWatchedEpisode != null)
{
sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => episode.Id != lastWatchedEpisode.Id).Skip(1);
}
nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
}
if (nextEpisode != null)
{
var userData = _userDataManager.GetUserData(user, nextEpisode);

View File

@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
@@ -194,7 +195,7 @@ namespace Emby.Server.Implementations.Updates
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
if (plugin != null)
{
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path);
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
}
// Remove versions with a target ABI greater then the current application version.
@@ -500,7 +501,8 @@ namespace Emby.Server.Implementations.Updates
var plugins = _pluginManager.Plugins;
foreach (var plugin in plugins)
{
if (plugin.Manifest?.AutoUpdate == false)
// Don't auto update when plugin marked not to, or when it's disabled.
if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
{
continue;
}
@@ -515,7 +517,7 @@ namespace Emby.Server.Implementations.Updates
}
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{
var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
@@ -567,7 +569,7 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir);
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_pluginManager.ImportPluginFrom(targetDir);
}
@@ -576,7 +578,7 @@ namespace Emby.Server.Implementations.Updates
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false);
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
return plugin != null;

View File

@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -168,22 +168,22 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
@@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -333,22 +333,22 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,

View File

@@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -219,14 +219,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
var streamingRequest = new HlsVideoRequestDto
{
Id = itemId,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -250,28 +250,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -386,14 +386,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
var streamingRequest = new HlsAudioRequestDto
{
Id = itemId,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -417,28 +417,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -534,7 +534,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -549,14 +549,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
{
Id = itemId,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -580,28 +580,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -699,7 +699,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -714,14 +714,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new StreamingRequestDto
{
Id = itemId,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -745,28 +745,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -869,7 +869,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -884,14 +884,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var streamingRequest = new VideoRequestDto
{
Id = itemId,
Container = container,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -915,28 +915,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -1041,7 +1041,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -1056,14 +1056,14 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var streamingRequest = new StreamingRequestDto
{
Id = itemId,
Container = container,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -1087,28 +1087,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};

View File

@@ -63,7 +63,13 @@ namespace Jellyfin.Api.Controllers
{
// TODO: Deprecate with new iOS app
var file = segmentId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.Ordinal))
{
return BadRequest("Invalid segment.");
}
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
}
@@ -83,7 +89,13 @@ namespace Jellyfin.Api.Controllers
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = playlistId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.Ordinal) || Path.GetExtension(file) != ".m3u8")
{
return BadRequest("Invalid segment.");
}
return GetFileResult(file, file);
}
@@ -98,7 +110,9 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Videos/ActiveEncodings")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
public ActionResult StopEncodingProcess(
[FromQuery, Required] string deviceId,
[FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
return NoContent();
@@ -130,7 +144,12 @@ namespace Jellyfin.Api.Controllers
var file = segmentId + Path.GetExtension(Request.Path);
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.Combine(transcodeFolderPath, file);
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.Ordinal))
{
return BadRequest("Invalid segment.");
}
var normalizedPlaylistId = playlistId;

View File

@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
: type;
var path = BaseItem.SupportedImageExtensions
.Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
.Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i)))
.FirstOrDefault(System.IO.File.Exists);
if (path == null)
@@ -82,6 +82,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.Ordinal))
{
return BadRequest("Invalid image path.");
}
var contentType = MimeTypes.GetMimeType(path);
return File(System.IO.File.OpenRead(path), contentType);
}
@@ -163,7 +168,8 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
private ActionResult GetImageFile(string basePath, string theme, string? name)
{
var themeFolder = Path.Combine(basePath, theme);
var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme));
if (Directory.Exists(themeFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
@@ -171,12 +177,18 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
if (!path.StartsWith(basePath, StringComparison.Ordinal))
{
return BadRequest("Invalid image path.");
}
var contentType = MimeTypes.GetMimeType(path);
return PhysicalFile(path, contentType);
}
}
var allFolder = Path.Combine(basePath, "all");
var allFolder = Path.GetFullPath(Path.Combine(basePath, "all"));
if (Directory.Exists(allFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
@@ -184,6 +196,11 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
if (!path.StartsWith(basePath, StringComparison.Ordinal))
{
return BadRequest("Invalid image path.");
}
var contentType = MimeTypes.GetMimeType(path);
return PhysicalFile(path, contentType);
}

View File

@@ -392,7 +392,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] int newIndex)
[FromQuery, Required] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -480,6 +480,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
@@ -509,6 +511,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] string? tag,
[FromQuery] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
@@ -539,6 +543,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -560,6 +566,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
@@ -589,6 +597,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] string? tag,
[FromQuery] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
@@ -618,6 +628,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -638,6 +650,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
@@ -667,6 +681,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromRoute, Required] string tag,
[FromQuery] bool? cropWhitespace,
[FromRoute, Required] ImageFormat format,
@@ -697,6 +713,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -721,6 +739,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -741,7 +761,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetArtistImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -750,6 +770,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -776,6 +798,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -800,6 +824,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -820,7 +846,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -829,6 +855,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -855,6 +883,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -880,6 +910,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -900,7 +932,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -909,6 +941,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -934,6 +968,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -958,6 +994,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -978,7 +1016,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetMusicGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -987,6 +1025,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1013,6 +1053,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1038,6 +1080,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1058,7 +1102,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1067,6 +1111,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1092,6 +1138,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1116,6 +1164,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1136,7 +1186,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetPersonImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1145,6 +1195,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1171,6 +1223,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1196,6 +1250,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1216,7 +1272,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1225,6 +1281,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1250,6 +1308,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1274,6 +1334,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1303,6 +1365,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1329,6 +1393,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1354,6 +1420,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1383,6 +1451,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1408,6 +1478,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1432,6 +1504,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1461,6 +1535,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1504,6 +1580,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1530,6 +1608,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1559,6 +1639,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1601,6 +1683,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1685,6 +1769,8 @@ namespace Jellyfin.Api.Controllers
int? width,
int? height,
int? quality,
int? fillWidth,
int? fillHeight,
bool? cropWhitespace,
bool? addPlayedIndicator,
int? blur,
@@ -1748,11 +1834,13 @@ namespace Jellyfin.Api.Controllers
item,
itemId,
imageIndex,
height,
maxHeight,
maxWidth,
quality,
width,
height,
maxWidth,
maxHeight,
fillWidth,
fillHeight,
quality,
addPlayedIndicator,
percentPlayed,
unplayedCount,
@@ -1847,11 +1935,13 @@ namespace Jellyfin.Api.Controllers
BaseItem? item,
Guid itemId,
int? index,
int? height,
int? maxHeight,
int? maxWidth,
int? quality,
int? width,
int? height,
int? maxWidth,
int? maxHeight,
int? fillWidth,
int? fillHeight,
int? quality,
bool? addPlayedIndicator,
double? percentPlayed,
int? unplayedCount,
@@ -1880,6 +1970,8 @@ namespace Jellyfin.Api.Controllers
ItemId = itemId,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
FillHeight = fillHeight,
FillWidth = fillWidth,
Quality = quality ?? 100,
Width = width,
AddPlayedIndicator = addPlayedIndicator ?? false,

View File

@@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given album.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given playlist.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given genre.
/// </summary>
/// <param name="name">The genre name.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/{name}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
[FromRoute, Required] string name,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given artist.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -230,7 +230,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given genre.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
[FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -266,7 +266,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given item.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -301,6 +301,80 @@ namespace Jellyfin.Api.Controllers
return GetResult(items, user, limit, dtoOptions);
}
/// <summary>
/// Creates an instant playlist based on a given artist.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Artists/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Use GetInstantMixFromArtists")]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
return GetInstantMixFromArtists(
id,
userId,
limit,
fields,
enableImages,
enableUserData,
imageTypeLimit,
enableImageTypes);
}
/// <summary>
/// Creates an instant playlist based on a given genre.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Use GetInstantMixFromMusicGenres instead")]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
return GetInstantMixFromMusicGenreById(
id,
userId,
limit,
fields,
enableImages,
enableUserData,
imageTypeLimit,
enableImageTypes);
}
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
{
var list = items;

View File

@@ -239,48 +239,6 @@ namespace Jellyfin.Api.Controllers
return Ok(results);
}
/// <summary>
/// Gets a remote image.
/// </summary>
/// <param name="imageUrl">The image url.</param>
/// <param name="providerName">The provider name.</param>
/// <response code="200">Remote image retrieved.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
/// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
/// </returns>
[HttpGet("Items/RemoteSearch/Image")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesImageFile]
public async Task<ActionResult> GetRemoteSearchImage(
[FromQuery, Required] string imageUrl,
[FromQuery, Required] string providerName)
{
var urlHash = imageUrl.GetMD5();
var pointerCachePath = GetFullCachePath(urlHash.ToString());
try
{
var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
if (System.IO.File.Exists(contentPath))
{
return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath));
}
}
catch (FileNotFoundException)
{
// Means the file isn't cached yet
}
catch (IOException)
{
// Means the file isn't cached yet
}
await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath));
}
/// <summary>
/// Applies search criteria to an item and refreshes metadata.
/// </summary>
@@ -322,53 +280,5 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
/// <summary>
/// Downloads the image.
/// </summary>
/// <param name="providerName">Name of the provider.</param>
/// <param name="url">The URL.</param>
/// <param name="urlHash">The URL hash.</param>
/// <param name="pointerCachePath">The pointer cache path.</param>
/// <returns>Task.</returns>
private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
{
using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
if (result.Content.Headers.ContentType?.MediaType == null)
{
throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
}
var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(directory);
using (var stream = result.Content)
{
await using var fileStream = new FileStream(
fullCachePath,
FileMode.Create,
FileAccess.Write,
FileShare.Read,
IODefaults.FileStreamBufferSize,
true);
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
Directory.CreateDirectory(pointerCacheDirectory);
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
}
/// <summary>
/// Gets the full cache path.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
private string GetFullCachePath(string filename)
=> Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
}
}

View File

@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}/ContentType")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType)
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)

View File

@@ -247,8 +247,13 @@ namespace Jellyfin.Api.Controllers
folder = _libraryManager.GetUserRootFolder();
}
if (folder is IHasCollectionType hasCollectionType
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
string? collectionType = null;
if (folder is IHasCollectionType hasCollectionType)
{
collectionType = hasCollectionType.CollectionType;
}
if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{
recursive = true;
includeItemTypes = new[] { "Playlist" };
@@ -271,10 +276,11 @@ namespace Jellyfin.Api.Controllers
}
}
if (!(item is UserRootFolder)
if (item is not UserRootFolder
&& !isInEnabledFolder
&& !user.HasPermission(PermissionKind.EnableAllFolders)
&& !user.HasPermission(PermissionKind.EnableAllChannels))
&& !user.HasPermission(PermissionKind.EnableAllChannels)
&& !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");

View File

@@ -115,7 +115,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path));
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
}
/// <summary>
@@ -304,7 +304,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <response code="204">Library scan started.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpGet("Library/Refresh")]
[HttpPost("Library/Refresh")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RefreshLibrary()
@@ -591,15 +591,15 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Reports that new movies have been added by an external source.
/// </summary>
/// <param name="updates">A list of updated media paths.</param>
/// <param name="dto">The update paths.</param>
/// <response code="204">Report success.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Library/Media/Updated")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
{
foreach (var item in updates)
foreach (var item in dto.Updates)
{
_libraryMonitor.ReportFileSystemChanged(item.Path);
}
@@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers
}
// TODO determine non-ASCII validity.
return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
return PhysicalFile(path, MimeTypes.GetMimeType(path), filename, true);
}
/// <summary>
@@ -778,7 +778,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
[FromQuery] string? libraryContentType,
[FromQuery] bool isNewLibrary)
[FromQuery] bool isNewLibrary = false)
{
var result = new LibraryOptionsResultDto();

View File

@@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Updates a media path.
/// </summary>
/// <param name="name">The name of the library.</param>
/// <param name="pathInfo">The path info.</param>
/// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path updated.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
[HttpPost("Paths/Update")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaPath(
[FromQuery] string? name,
[FromBody] MediaPathInfo? pathInfo)
public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
{
if (string.IsNullOrWhiteSpace(name))
if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
{
throw new ArgumentNullException(nameof(name));
throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
}
_libraryManager.UpdateMediaPath(name, pathInfo);
_libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
return NoContent();
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using Jellyfin.Api.Constants;
@@ -86,26 +87,19 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Sends a notification to all admins.
/// </summary>
/// <param name="url">The URL of the notification.</param>
/// <param name="level">The level of the notification.</param>
/// <param name="name">The name of the notification.</param>
/// <param name="description">The description of the notification.</param>
/// <param name="notificationDto">The notification request.</param>
/// <response code="204">Notification sent.</response>
/// <returns>A <cref see="NoContentResult"/>.</returns>
[HttpPost("Admin")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CreateAdminNotification(
[FromQuery] string? url,
[FromQuery] NotificationLevel? level,
[FromQuery] string name = "",
[FromQuery] string description = "")
public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto)
{
var notification = new NotificationRequest
{
Name = name,
Description = description,
Url = url,
Level = level ?? NotificationLevel.Normal,
Name = notificationDto.Name,
Description = notificationDto.Description,
Url = notificationDto.Url,
Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal,
UserIds = _userManager.Users
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
.Select(user => user.Id)
@@ -114,7 +108,6 @@ namespace Jellyfin.Api.Controllers
};
_notificationManager.SendNotification(notification, CancellationToken.None);
return NoContent();
}

View File

@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/Playing/Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
return NoContent();
@@ -202,9 +202,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] PlayMethod playMethod,
[FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
[FromQuery] string playSessionId,
[FromQuery] string? playSessionId,
[FromQuery] bool canSeek = false)
{
var playbackStartInfo = new PlaybackStartInfo
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
MediaSourceId = mediaSourceId,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
PlayMethod = playMethod,
PlayMethod = playMethod ?? PlayMethod.Transcode,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId
};
@@ -254,10 +254,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? volumeLevel,
[FromQuery] PlayMethod playMethod,
[FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
[FromQuery] string playSessionId,
[FromQuery] RepeatMode repeatMode,
[FromQuery] string? playSessionId,
[FromQuery] RepeatMode? repeatMode,
[FromQuery] bool isPaused = false,
[FromQuery] bool isMuted = false)
{
@@ -271,10 +271,10 @@ namespace Jellyfin.Api.Controllers
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
VolumeLevel = volumeLevel,
PlayMethod = playMethod,
PlayMethod = playMethod ?? PlayMethod.Transcode,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId,
RepeatMode = repeatMode
RepeatMode = repeatMode ?? RepeatMode.RepeatNone
};
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
@@ -352,7 +352,7 @@ namespace Jellyfin.Api.Controllers
return _userDataRepository.GetUserDataDto(item, user);
}
private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
{
if (method == PlayMethod.Transcode)
{

View File

@@ -145,58 +145,6 @@ namespace Jellyfin.Api.Controllers
return Ok(_providerManager.GetRemoteImageProviderInfo(item));
}
/// <summary>
/// Gets a remote image.
/// </summary>
/// <param name="imageUrl">The image url.</param>
/// <response code="200">Remote image returned.</response>
/// <response code="404">Remote image not found.</response>
/// <returns>Image Stream.</returns>
[HttpGet("Images/Remote")]
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl)
{
var urlHash = imageUrl.ToString().GetMD5();
var pointerCachePath = GetFullCachePath(urlHash.ToString());
string? contentPath = null;
var hasFile = false;
try
{
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
if (System.IO.File.Exists(contentPath))
{
hasFile = true;
}
}
catch (FileNotFoundException)
{
// The file isn't cached yet
}
catch (IOException)
{
// The file isn't cached yet
}
if (!hasFile)
{
await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
}
if (string.IsNullOrEmpty(contentPath))
{
return NotFound();
}
var contentType = MimeTypes.GetMimeType(contentPath);
return PhysicalFile(contentPath, contentType);
}
/// <summary>
/// Downloads a remote image for an item.
/// </summary>
@@ -259,7 +207,8 @@ namespace Jellyfin.Api.Controllers
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(fullCacheDirectory);
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));

View File

@@ -153,6 +153,10 @@ namespace Jellyfin.Api.Controllers
/// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
/// <param name="itemIds">The ids of the items to play, comma delimited.</param>
/// <param name="startPositionTicks">The starting position of the first item.</param>
/// <param name="mediaSourceId">Optional. The media source id.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to play.</param>
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to play.</param>
/// <param name="startIndex">Optional. The start index.</param>
/// <response code="204">Instruction sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Playing")]
@@ -162,13 +166,21 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
[FromQuery] long? startPositionTicks)
[FromQuery] long? startPositionTicks,
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? startIndex)
{
var playRequest = new PlayRequest
{
ItemIds = itemIds,
StartPositionTicks = startPositionTicks,
PlayCommand = playCommand
PlayCommand = playCommand,
MediaSourceId = mediaSourceId,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
StartIndex = startIndex
};
_sessionManager.SendPlayCommand(
@@ -301,9 +313,7 @@ namespace Jellyfin.Api.Controllers
/// Issues a command to a client to display a message to the user.
/// </summary>
/// <param name="sessionId">The session id.</param>
/// <param name="text">The message test.</param>
/// <param name="header">The message header.</param>
/// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param>
/// <param name="command">The <see cref="MessageCommand" /> object containing Header, Message Text, and TimeoutMs.</param>
/// <response code="204">Message sent.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Message")]
@@ -311,16 +321,12 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendMessageCommand(
[FromRoute, Required] string sessionId,
[FromQuery, Required] string text,
[FromQuery] string? header,
[FromQuery] long? timeoutMs)
[FromBody, Required] MessageCommand command)
{
var command = new MessageCommand
if (string.IsNullOrWhiteSpace(command.Header))
{
Header = string.IsNullOrEmpty(header) ? "Message from Server" : header,
TimeoutMs = timeoutMs,
Text = text
};
command.Header = "Message from Server";
}
_sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);

View File

@@ -182,6 +182,10 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets subtitles in a specified format.
/// </summary>
/// <param name="routeItemId">The (route) item id.</param>
/// <param name="routeMediaSourceId">The (route) media source id.</param>
/// <param name="routeIndex">The (route) subtitle stream index.</param>
/// <param name="routeFormat">The (route) format of the returned subtitle.</param>
/// <param name="itemId">The item id.</param>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="index">The subtitle stream index.</param>
@@ -189,22 +193,32 @@ namespace Jellyfin.Api.Controllers
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
/// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
[HttpGet("Videos/{routeItemId}/routeMediaSourceId/Subtitles/{routeIndex}/Stream.{routeFormat}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("text/*")]
public async Task<ActionResult> GetSubtitle(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string mediaSourceId,
[FromRoute, Required] int index,
[FromRoute, Required] string format,
[FromRoute, Required] Guid routeItemId,
[FromRoute, Required] string routeMediaSourceId,
[FromRoute, Required] int routeIndex,
[FromRoute, Required] string routeFormat,
[FromQuery, ParameterObsolete] Guid? itemId,
[FromQuery, ParameterObsolete] string? mediaSourceId,
[FromQuery, ParameterObsolete] int? index,
[FromQuery, ParameterObsolete] string? format,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps = false,
[FromQuery] bool addVttTimeMap = false,
[FromQuery] long startPositionTicks = 0)
{
// Set parameters to route value if not provided via query.
itemId ??= routeItemId;
mediaSourceId ??= routeMediaSourceId;
index ??= routeIndex;
format ??= routeFormat;
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
{
format = "json";
@@ -212,9 +226,9 @@ namespace Jellyfin.Api.Controllers
if (string.IsNullOrEmpty(format))
{
var item = (Video)_libraryManager.GetItemById(itemId);
var item = (Video)_libraryManager.GetItemById(itemId.Value);
var idString = itemId.ToString("N", CultureInfo.InvariantCulture);
var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
.First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
@@ -226,7 +240,7 @@ namespace Jellyfin.Api.Controllers
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
{
await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
using var reader = new StreamReader(stream);
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
@@ -238,9 +252,9 @@ namespace Jellyfin.Api.Controllers
return File(
await EncodeSubtitles(
itemId,
itemId.Value,
mediaSourceId,
index,
index.Value,
format,
startPositionTicks,
endPositionTicks,
@@ -251,30 +265,44 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets subtitles in a specified format.
/// </summary>
/// <param name="routeItemId">The (route) item id.</param>
/// <param name="routeMediaSourceId">The (route) media source id.</param>
/// <param name="routeIndex">The (route) subtitle stream index.</param>
/// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param>
/// <param name="routeFormat">The (route) format of the returned subtitle.</param>
/// <param name="itemId">The item id.</param>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="index">The subtitle stream index.</param>
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
/// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
/// <param name="format">The format of the returned subtitle.</param>
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
[HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("text/*")]
public Task<ActionResult> GetSubtitleWithTicks(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string mediaSourceId,
[FromRoute, Required] int index,
[FromRoute, Required] long startPositionTicks,
[FromRoute, Required] string format,
[FromRoute, Required] Guid routeItemId,
[FromRoute, Required] string routeMediaSourceId,
[FromRoute, Required] int routeIndex,
[FromRoute, Required] long routeStartPositionTicks,
[FromRoute, Required] string routeFormat,
[FromQuery, ParameterObsolete] Guid? itemId,
[FromQuery, ParameterObsolete] string? mediaSourceId,
[FromQuery, ParameterObsolete] int? index,
[FromQuery, ParameterObsolete] long? startPositionTicks,
[FromQuery, ParameterObsolete] string? format,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps = false,
[FromQuery] bool addVttTimeMap = false)
{
return GetSubtitle(
routeItemId,
routeMediaSourceId,
routeIndex,
routeFormat,
itemId,
mediaSourceId,
index,
@@ -282,7 +310,7 @@ namespace Jellyfin.Api.Controllers
endPositionTicks,
copyTimestamps,
addVttTimeMap,
startPositionTicks);
startPositionTicks ?? routeStartPositionTicks);
}
/// <summary>

View File

@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] bool? enableRemoteMedia,
[FromQuery] bool breakOnNonKeyFrames,
[FromQuery] bool breakOnNonKeyFrames = false,
[FromQuery] bool enableRedirection = true)
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
@@ -219,11 +219,11 @@ namespace Jellyfin.Api.Controllers
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Hls,
RequireAvc = true,
DeInterlace = true,
RequireNonAnamorphic = true,
EnableMpegtsM2TsMode = true,
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
RequireAvc = false,
DeInterlace = false,
RequireNonAnamorphic = false,
EnableMpegtsM2TsMode = false,
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
Context = EncodingContext.Static,
StreamOptions = new Dictionary<string, string>(),
EnableAdaptiveBitrateStreaming = true
@@ -251,7 +251,7 @@ namespace Jellyfin.Api.Controllers
AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = maxAudioChannels,
CopyTimestamps = true,
CopyTimestamps = false,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Embed,
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),

View File

@@ -199,7 +199,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -224,7 +224,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -248,28 +248,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
MaxHeight = maxHeight,
MaxWidth = maxWidth,

View File

@@ -217,9 +217,7 @@ namespace Jellyfin.Api.Controllers
return BadRequest("Please supply at least two videos to merge.");
}
var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList();
var primaryVersion = videosWithVersions.FirstOrDefault();
var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
if (primaryVersion == null)
{
primaryVersion = items
@@ -364,7 +362,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -379,7 +377,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
@@ -388,7 +386,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
Static = @static ?? true,
Static = @static ?? false,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@@ -412,28 +410,28 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
CopyTimestamps = copyTimestamps ?? false,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
RequireAvc = requireAvc ?? false,
DeInterlace = deInterlace ?? false,
RequireNonAnamorphic = requireNonAnamorphic ?? false,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -540,7 +538,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param>
/// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -569,7 +567,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@@ -583,8 +581,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/{stream=stream}.{container}")]
[HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
[HttpGet("{itemId}/stream.{container}")]
[HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesVideoFile]
public Task<ActionResult> GetVideoStreamByContainer(
@@ -620,7 +618,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -635,7 +633,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
return GetVideoStream(

View File

@@ -46,7 +46,8 @@ namespace Jellyfin.Api.Helpers
if (isHeadRequest)
{
return new FileContentResult(Array.Empty<byte>(), contentType);
httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
return new OkResult();
}
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
@@ -68,10 +69,10 @@ namespace Jellyfin.Api.Helpers
{
httpContext.Response.ContentType = contentType;
// if the request is a head request, return a NoContent result with the same headers as it would with a GET request
// if the request is a head request, return an OkResult (200) with the same headers as it would with a GET request
if (isHeadRequest)
{
return new NoContentResult();
return new OkResult();
}
return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
@@ -107,7 +108,8 @@ namespace Jellyfin.Api.Helpers
// Headers only
if (isHeadRequest)
{
return new FileContentResult(Array.Empty<byte>(), contentType);
httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
return new OkResult();
}
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);

View File

@@ -282,6 +282,7 @@ namespace Jellyfin.Api.Helpers
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
}
@@ -307,7 +308,7 @@ namespace Jellyfin.Api.Helpers
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
&& user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectStream = true;
}
@@ -326,6 +327,7 @@ namespace Jellyfin.Api.Helpers
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
}
@@ -353,6 +355,7 @@ namespace Jellyfin.Api.Helpers
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
else
@@ -390,6 +393,7 @@ namespace Jellyfin.Api.Helpers
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
}
}
}

View File

@@ -508,17 +508,15 @@ namespace Jellyfin.Api.Helpers
private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
{
var headers = request.Headers;
if (!string.IsNullOrWhiteSpace(deviceProfileId))
{
state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
}
else if (!string.IsNullOrWhiteSpace(deviceProfileId))
{
var caps = deviceManager.GetCapabilities(deviceProfileId);
state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
if (state.DeviceProfile == null)
{
var caps = deviceManager.GetCapabilities(deviceProfileId);
state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
}
}
var profile = state.DeviceProfile;

View File

@@ -1,4 +1,7 @@
namespace Jellyfin.Api.Models.LibraryDtos
using System;
using System.Collections.Generic;
namespace Jellyfin.Api.Models.LibraryDtos
{
/// <summary>
/// Media Update Info Dto.
@@ -6,14 +9,8 @@
public class MediaUpdateInfoDto
{
/// <summary>
/// Gets or sets media path.
/// Gets or sets the list of updates.
/// </summary>
public string? Path { get; set; }
/// <summary>
/// Gets or sets media update type.
/// Created, Modified, Deleted.
/// </summary>
public string? UpdateType { get; set; }
public IReadOnlyList<MediaUpdateInfoPathDto> Updates { get; set; } = Array.Empty<MediaUpdateInfoPathDto>();
}
}

View File

@@ -0,0 +1,19 @@
namespace Jellyfin.Api.Models.LibraryDtos
{
/// <summary>
/// The media update info path.
/// </summary>
public class MediaUpdateInfoPathDto
{
/// <summary>
/// Gets or sets media path.
/// </summary>
public string? Path { get; set; }
/// <summary>
/// Gets or sets media update type.
/// Created, Modified, Deleted.
/// </summary>
public string? UpdateType { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
using MediaBrowser.Model.Configuration;
namespace Jellyfin.Api.Models.LibraryStructureDto
{
/// <summary>
/// Update library options dto.
/// </summary>
public class UpdateMediaPathRequestDto
{
/// <summary>
/// Gets or sets the library name.
/// </summary>
[Required]
public string Name { get; set; } = null!;
/// <summary>
/// Gets or sets library folder path information.
/// </summary>
[Required]
public MediaPathInfo PathInfo { get; set; } = null!;
}
}

View File

@@ -0,0 +1,30 @@
using MediaBrowser.Model.Notifications;
namespace Jellyfin.Api.Models.NotificationDtos
{
/// <summary>
/// The admin notification dto.
/// </summary>
public class AdminNotificationDto
{
/// <summary>
/// Gets or sets the notification name.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the notification description.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the notification level.
/// </summary>
public NotificationLevel? NotificationLevel { get; set; }
/// <summary>
/// Gets or sets the notification url.
/// </summary>
public string? Url { get; set; }
}
}

View File

@@ -19,7 +19,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.7.0</VersionPrefix>
<VersionPrefix>10.7.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -165,7 +165,7 @@ namespace Jellyfin.Networking.Manager
{
foreach (var item in source)
{
result.AddItem(item);
result.AddItem(item, false);
}
}
@@ -274,7 +274,7 @@ namespace Jellyfin.Networking.Manager
if (_bindExclusions.Count > 0)
{
// Return all the interfaces except the ones specifically excluded.
return _interfaceAddresses.Exclude(_bindExclusions);
return _interfaceAddresses.Exclude(_bindExclusions, false);
}
if (individualInterfaces)
@@ -285,21 +285,32 @@ namespace Jellyfin.Networking.Manager
// No bind address and no exclusions, so listen on all interfaces.
Collection<IPObject> result = new Collection<IPObject>();
if (IsIP4Enabled)
if (IsIP6Enabled && IsIP4Enabled)
{
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any
result.AddItem(IPAddress.IPv6Any);
}
else if (IsIP4Enabled)
{
result.AddItem(IPAddress.Any);
}
if (IsIP6Enabled)
else if (IsIP6Enabled)
{
result.AddItem(IPAddress.IPv6Any);
// Cannot use IPv6Any as Kestrel will bind to IPv4 addresses.
foreach (var iface in _interfaceAddresses)
{
if (iface.AddressFamily == AddressFamily.InterNetworkV6)
{
result.AddItem(iface.Address);
}
}
}
return result;
}
// Remove any excluded bind interfaces.
return _bindAddresses.Exclude(_bindExclusions);
return _bindAddresses.Exclude(_bindExclusions, false);
}
/// <inheritdoc/>
@@ -386,15 +397,26 @@ namespace Jellyfin.Networking.Manager
}
// Get the first LAN interface address that isn't a loopback.
var interfaces = CreateCollection(_interfaceAddresses
.Exclude(_bindExclusions)
.Where(IsInLocalNetwork)
.OrderBy(p => p.Tag));
var interfaces = CreateCollection(
_interfaceAddresses
.Exclude(_bindExclusions, false)
.Where(IsInLocalNetwork)
.OrderBy(p => p.Tag));
if (interfaces.Count > 0)
{
if (haveSource)
{
foreach (var intf in interfaces)
{
if (intf.Address.Equals(source.Address))
{
result = FormatIP6String(intf.Address);
_logger.LogDebug("{Source}: GetBindInterface: Has found matching interface. {Result}", source, result);
return result;
}
}
// Does the request originate in one of the interface subnets?
// (For systems with multiple internal network cards, and multiple subnets)
foreach (var intf in interfaces)
@@ -414,7 +436,7 @@ namespace Jellyfin.Networking.Manager
}
// There isn't any others, so we'll use the loopback.
result = IsIP6Enabled ? "::" : "127.0.0.1";
result = IsIP6Enabled ? "::1" : "127.0.0.1";
_logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result);
return result;
}
@@ -521,10 +543,10 @@ namespace Jellyfin.Networking.Manager
{
if (filter == null)
{
return _lanSubnets.Exclude(_excludedSubnets).AsNetworks();
return _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks();
}
return _lanSubnets.Exclude(filter);
return _lanSubnets.Exclude(filter, true);
}
/// <inheritdoc/>
@@ -555,7 +577,7 @@ namespace Jellyfin.Networking.Manager
&& ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork)
|| (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
{
result.AddItem(iface);
result.AddItem(iface, false);
}
}
@@ -565,6 +587,29 @@ namespace Jellyfin.Networking.Manager
return false;
}
/// <inheritdoc/>
public bool HasRemoteAccess(IPAddress remoteIp)
{
var config = _configurationManager.GetNetworkConfiguration();
if (config.EnableRemoteAccess)
{
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
// If left blank, all remote addresses will be allowed.
if (RemoteAddressFilter.Count > 0 && !IsInLocalNetwork(remoteIp))
{
// remoteAddressFilter is a whitelist or blacklist.
return RemoteAddressFilter.ContainsAddress(remoteIp) == !config.IsRemoteIPFilterBlacklist;
}
}
else if (!IsInLocalNetwork(remoteIp))
{
// Remote not enabled. So everyone should be LAN.
return false;
}
return true;
}
/// <summary>
/// Reloads all settings and re-initialises the instance.
/// </summary>
@@ -599,8 +644,8 @@ namespace Jellyfin.Networking.Manager
var address = IPNetAddress.Parse(parts[0]);
var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
address.Tag = index;
_interfaceAddresses.AddItem(address);
_interfaceNames.Add(parts[2], Math.Abs(index));
_interfaceAddresses.AddItem(address, false);
_interfaceNames[parts[2]] = Math.Abs(index);
}
}
@@ -1017,7 +1062,7 @@ namespace Jellyfin.Networking.Manager
_logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString());
_logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString());
_logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks().AsString());
_logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks().AsString());
}
}
@@ -1071,7 +1116,7 @@ namespace Jellyfin.Networking.Manager
nw.Tag *= -1;
}
_interfaceAddresses.AddItem(nw);
_interfaceAddresses.AddItem(nw, false);
// Store interface name so we can use the name in Collections.
_interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
@@ -1092,7 +1137,7 @@ namespace Jellyfin.Networking.Manager
nw.Tag *= -1;
}
_interfaceAddresses.AddItem(nw);
_interfaceAddresses.AddItem(nw, false);
// Store interface name so we can use the name in Collections.
_interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
@@ -1126,10 +1171,10 @@ namespace Jellyfin.Networking.Manager
{
_logger.LogWarning("No interfaces information available. Using loopback.");
// Last ditch attempt - use loopback address.
_interfaceAddresses.AddItem(IPNetAddress.IP4Loopback);
_interfaceAddresses.AddItem(IPNetAddress.IP4Loopback, false);
if (IsIP6Enabled)
{
_interfaceAddresses.AddItem(IPNetAddress.IP6Loopback);
_interfaceAddresses.AddItem(IPNetAddress.IP6Loopback, false);
}
}
}
@@ -1206,7 +1251,7 @@ namespace Jellyfin.Networking.Manager
private bool MatchesBindInterface(IPObject source, bool isInExternalSubnet, out string result)
{
result = string.Empty;
var addresses = _bindAddresses.Exclude(_bindExclusions);
var addresses = _bindAddresses.Exclude(_bindExclusions, false);
int count = addresses.Count;
if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any)))
@@ -1291,7 +1336,7 @@ namespace Jellyfin.Networking.Manager
result = string.Empty;
// Get the first WAN interface address that isn't a loopback.
var extResult = _interfaceAddresses
.Exclude(_bindExclusions)
.Exclude(_bindExclusions, false)
.Where(p => !IsInLocalNetwork(p))
.OrderBy(p => p.Tag);

View File

@@ -0,0 +1,520 @@
#pragma warning disable CS1591
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDb))]
[Migration("20210407110544_NullableCustomPrefValue")]
partial class NullableCustomPrefValue
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "5.0.3");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DayOfWeek")
.HasColumnType("INTEGER");
b.Property<double>("EndHour")
.HasColumnType("REAL");
b.Property<double>("StartHour")
.HasColumnType("REAL");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AccessSchedules");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
b.ToTable("CustomItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChromecastVersion")
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<string>("DashboardTheme")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("ScrollDirection")
.HasColumnType("INTEGER");
b.Property<bool>("ShowBackdrop")
.HasColumnType("INTEGER");
b.Property<bool>("ShowSidebar")
.HasColumnType("INTEGER");
b.Property<int>("SkipBackwardLength")
.HasColumnType("INTEGER");
b.Property<int>("SkipForwardLength")
.HasColumnType("INTEGER");
b.Property<string>("TvHome")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DisplayPreferencesId")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<bool>("RememberIndexing")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSorting")
.HasColumnType("INTEGER");
b.Property<string>("SortBy")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<int>("ViewType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Permission_Permissions_Guid");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Preference_Preferences_Guid");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<string>("EasyPassword")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUserPreferenceAccess")
.HasColumnType("INTEGER");
b.Property<bool>("HidePlayedInLatest")
.HasColumnType("INTEGER");
b.Property<long>("InternalId")
.HasColumnType("INTEGER");
b.Property<int>("InvalidLoginAttemptCount")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int>("SyncPlayAccess")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("AccessSchedules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("DisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
.WithMany("HomeSections")
.HasForeignKey("DisplayPreferencesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("ItemDisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("Permission_Permissions_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("Preference_Preferences_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences");
b.Navigation("ItemDisplayPreferences");
b.Navigation("Permissions");
b.Navigation("Preferences");
b.Navigation("ProfileImage");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,35 @@
#pragma warning disable CS1591
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
namespace Jellyfin.Server.Implementations.Migrations
{
public partial class NullableCustomPrefValue : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Value",
schema: "jellyfin",
table: "CustomItemDisplayPreferences",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Value",
schema: "jellyfin",
table: "CustomItemDisplayPreferences",
type: "TEXT",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "5.0.0");
.HasAnnotation("ProductVersion", "5.0.3");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -110,7 +110,6 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
@@ -448,8 +447,8 @@ namespace Jellyfin.Server.Implementations.Migrations
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("DisplayPreferences")
.HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
.WithMany("DisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@@ -502,8 +501,7 @@ namespace Jellyfin.Server.Implementations.Migrations
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences")
.IsRequired();
b.Navigation("DisplayPreferences");
b.Navigation("ItemDisplayPreferences");

View File

@@ -67,7 +67,7 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc />
public IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
public Dictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
return _dbContext.CustomItemDisplayPreferences
.AsQueryable()

View File

@@ -21,6 +21,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -37,18 +38,21 @@ namespace Jellyfin.Server
/// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public CoreAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection collection)
: base(
applicationPaths,
loggerFactory,
options,
startupConfig,
fileSystem,
collection)
{

View File

@@ -261,15 +261,16 @@ namespace Jellyfin.Server.Extensions
{
return serviceCollection.AddSwaggerGen(c =>
{
var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1";
c.SwaggerDoc("api-docs", new OpenApiInfo
{
Title = "Jellyfin API",
Version = "v1",
Version = version,
Extensions = new Dictionary<string, IOpenApiExtension>
{
{
"x-jellyfin-version",
new OpenApiString(typeof(ApplicationHost).Assembly.GetName().Version?.ToString())
new OpenApiString(version)
}
}
});
@@ -318,7 +319,7 @@ namespace Jellyfin.Server.Extensions
c.OperationFilter<FileResponseFilter>();
c.OperationFilter<FileRequestFilter>();
c.OperationFilter<ParameterObsoleteFilter>();
c.DocumentFilter<WebsocketModelFilter>();
c.DocumentFilter<AdditionalModelFilter>();
});
}

View File

@@ -1,5 +1,6 @@
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.ApiClient;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
@@ -9,9 +10,9 @@ using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
{
/// <summary>
/// Add models used in websocket messaging.
/// Add models not directly used by the API, but used for discovery and websockets.
/// </summary>
public class WebsocketModelFilter : IDocumentFilter
public class AdditionalModelFilter : IDocumentFilter
{
/// <inheritdoc />
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
@@ -25,6 +26,9 @@ namespace Jellyfin.Server.Filters
context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
}
}
}

View File

@@ -9,6 +9,7 @@
<AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<ServerGarbageCollection>false</ServerGarbageCollection>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View File

@@ -29,9 +29,8 @@ namespace Jellyfin.Server.Middleware
/// </summary>
/// <param name="httpContext">The current HTTP context.</param>
/// <param name="networkManager">The network manager.</param>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
public async Task Invoke(HttpContext httpContext, INetworkManager networkManager)
{
if (httpContext.IsLocal())
{
@@ -42,32 +41,8 @@ namespace Jellyfin.Server.Middleware
var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
if (!networkManager.HasRemoteAccess(remoteIp))
{
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
// If left blank, all remote addresses will be allowed.
var remoteAddressFilter = networkManager.RemoteAddressFilter;
if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp))
{
// remoteAddressFilter is a whitelist or blacklist.
bool isListed = remoteAddressFilter.ContainsAddress(remoteIp);
if (!serverConfigurationManager.GetNetworkConfiguration().IsRemoteIPFilterBlacklist)
{
// Black list, so flip over.
isListed = !isListed;
}
if (!isListed)
{
// If your name isn't on the list, you arn't coming in.
return;
}
}
}
else if (!networkManager.IsInLocalNetwork(remoteIp))
{
// Remote not enabled. So everyone should be LAN.
return;
}

View File

@@ -41,9 +41,9 @@ namespace Jellyfin.Server.Migrations.Routines
var databasePath = Path.Join(_serverApplicationPaths.DataPath, DbFilename);
using var connection = SQLite3.Open(databasePath, ConnectionFlags.ReadWrite, null);
_logger.LogInformation("Creating index idx_TypedBaseItemsUserDataKeyType");
connection.Execute("CREATE INDEX idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);");
connection.Execute("CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);");
_logger.LogInformation("Creating index idx_PeopleNameListOrder");
connection.Execute("CREATE INDEX idx_PeopleNameListOrder ON People(Name, ListOrder);");
connection.Execute("CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder);");
}
}
}
}

View File

@@ -126,13 +126,13 @@ namespace Jellyfin.Server.Migrations.Routines
ShowSidebar = dto.ShowSidebar,
ScrollDirection = dto.ScrollDirection,
ChromecastVersion = chromecastVersion,
SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length)
? int.Parse(length, CultureInfo.InvariantCulture)
SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryParse(length, out var skipForwardLength)
? skipForwardLength
: 30000,
SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
? int.Parse(length, CultureInfo.InvariantCulture)
SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && !string.IsNullOrEmpty(length) && int.TryParse(length, out var skipBackwardLength)
? skipBackwardLength
: 10000,
EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled)
? bool.Parse(enabled)
: true,
DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty,

View File

@@ -164,6 +164,7 @@ namespace Jellyfin.Server
appPaths,
_loggerFactory,
options,
startupConfig,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
serviceCollection);
@@ -198,7 +199,7 @@ namespace Jellyfin.Server
}
catch
{
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in system.xml and try again.");
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again.");
throw;
}
@@ -280,7 +281,7 @@ namespace Jellyfin.Server
bool flagged = false;
foreach (IPObject netAdd in addresses)
{
_logger.LogInformation("Kestrel listening on {0}", netAdd);
_logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd);
options.Listen(netAdd.Address, appHost.HttpPort);
if (appHost.ListenWithHttps)
{

View File

@@ -77,7 +77,7 @@ namespace Jellyfin.Server
/// <inheritdoc />
[Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")]
public Uri? PublishedServerUrl { get; set; }
public string? PublishedServerUrl { get; set; }
/// <summary>
/// Gets the command line options as a dictionary that can be used in the .NET configuration system.
@@ -94,7 +94,7 @@ namespace Jellyfin.Server
if (PublishedServerUrl != null)
{
config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl.ToString());
config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl);
}
if (FFmpegPath != null)

View File

@@ -0,0 +1,39 @@
using System;
using System.Buffers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Converter to allow the serializer to read strings.
/// </summary>
public class JsonStringConverter : JsonConverter<string>
{
/// <inheritdoc />
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.String => reader.GetString(),
_ => GetRawValue(reader)
};
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
private static string GetRawValue(Utf8JsonReader reader)
{
var utf8Bytes = reader.HasValueSequence
? reader.ValueSequence.ToArray()
: reader.ValueSpan;
return Encoding.UTF8.GetString(utf8Bytes);
}
}
}

View File

@@ -40,7 +40,8 @@ namespace MediaBrowser.Common.Json
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
new JsonBoolNumberConverter(),
new JsonDateTimeConverter()
new JsonDateTimeConverter(),
new JsonStringConverter()
}
};

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.7.0</VersionPrefix>
<VersionPrefix>10.7.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -229,5 +229,12 @@ namespace MediaBrowser.Common.Net
/// <param name="filter">Optional filter for the list.</param>
/// <returns>Returns a filtered list of LAN addresses.</returns>
Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null);
/// <summary>
/// Checks to see if <paramref name="remoteIp"/> has access.
/// </summary>
/// <param name="remoteIp">IP Address of client.</param>
/// <returns><b>True</b> if has access, otherwise <b>false</b>.</returns>
bool HasRemoteAccess(IPAddress remoteIp);
}
}

View File

@@ -32,9 +32,11 @@ namespace MediaBrowser.Common.Net
/// </summary>
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
/// <param name="item">Item to add.</param>
public static void AddItem(this Collection<IPObject> source, IPObject item)
/// <param name="itemsAreNetworks">If <c>true</c> the values are treated as subnets.
/// If <b>false</b> items are addresses.</param>
public static void AddItem(this Collection<IPObject> source, IPObject item, bool itemsAreNetworks = true)
{
if (!source.ContainsAddress(item))
if (!source.ContainsAddress(item) || !itemsAreNetworks)
{
source.Add(item);
}
@@ -195,8 +197,9 @@ namespace MediaBrowser.Common.Net
/// </summary>
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
/// <param name="excludeList">Items to exclude.</param>
/// <param name="isNetwork">Collection is a network collection.</param>
/// <returns>A new collection, with the items excluded.</returns>
public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList)
public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList, bool isNetwork)
{
if (source.Count == 0 || excludeList == null)
{
@@ -221,7 +224,7 @@ namespace MediaBrowser.Common.Net
if (!found)
{
results.AddItem(outer);
results.AddItem(outer, isNetwork);
}
}

View File

@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
@@ -51,8 +52,9 @@ namespace MediaBrowser.Common.Plugins
/// <param name="packageInfo">The <see cref="PackageInfo"/> used to generate a manifest.</param>
/// <param name="version">Version to be installed.</param>
/// <param name="path">The path where to save the manifest.</param>
/// <param name="status">Initial status of the plugin.</param>
/// <returns>True if successful.</returns>
Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path);
Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status);
/// <summary>
/// Imports plugin details from a folder.

View File

@@ -1,4 +1,5 @@
#pragma warning disable CS1591
#nullable enable
using System;
using MediaBrowser.Controller.Entities;
@@ -9,67 +10,12 @@ namespace MediaBrowser.Controller.Drawing
{
public static class ImageHelper
{
public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions? originalImageSize)
public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions originalImageSize)
{
if (originalImageSize.HasValue)
{
// Determine the output size based on incoming parameters
var newSize = DrawingUtils.Resize(originalImageSize.Value, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
return newSize;
}
return GetSizeEstimate(options);
}
private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options)
{
if (options.Width.HasValue && options.Height.HasValue)
{
return new ImageDimensions(options.Width.Value, options.Height.Value);
}
double aspect = GetEstimatedAspectRatio(options.Image.Type, options.Item);
int? width = options.Width ?? options.MaxWidth;
if (width.HasValue)
{
int heightValue = Convert.ToInt32((double)width.Value / aspect);
return new ImageDimensions(width.Value, heightValue);
}
var height = options.Height ?? options.MaxHeight ?? 200;
int widthValue = Convert.ToInt32(aspect * height);
return new ImageDimensions(widthValue, height);
}
private static double GetEstimatedAspectRatio(ImageType type, BaseItem item)
{
switch (type)
{
case ImageType.Art:
case ImageType.Backdrop:
case ImageType.Chapter:
case ImageType.Screenshot:
case ImageType.Thumb:
return 1.78;
case ImageType.Banner:
return 5.4;
case ImageType.Box:
case ImageType.BoxRear:
case ImageType.Disc:
case ImageType.Menu:
case ImageType.Profile:
return 1;
case ImageType.Logo:
return 2.58;
case ImageType.Primary:
double defaultPrimaryImageAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
return defaultPrimaryImageAspectRatio > 0 ? defaultPrimaryImageAspectRatio : 2.0 / 3;
default:
return 1;
}
// Determine the output size based on incoming parameters
var newSize = DrawingUtils.Resize(originalImageSize, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
newSize = DrawingUtils.ResizeFill(newSize, options.FillWidth, options.FillHeight);
return newSize;
}
}
}

View File

@@ -34,6 +34,10 @@ namespace MediaBrowser.Controller.Drawing
public int? MaxHeight { get; set; }
public int? FillWidth { get; set; }
public int? FillHeight { get; set; }
public int Quality { get; set; }
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
@@ -95,6 +99,11 @@ namespace MediaBrowser.Controller.Drawing
return false;
}
if (sizeValue.Width > FillWidth || sizeValue.Height > FillHeight)
{
return false;
}
return true;
}

View File

@@ -1,6 +1,8 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Entities.Audio
{
@@ -23,15 +25,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public static IEnumerable<string> GetAllArtists<T>(this T item)
where T : IHasArtist, IHasAlbumArtist
{
foreach (var i in item.AlbumArtists)
{
yield return i;
}
foreach (var i in item.Artists)
{
yield return i;
}
return item.AlbumArtists.Concat(item.Artists).DistinctNames();
}
}
}

View File

@@ -1434,9 +1434,14 @@ namespace MediaBrowser.Controller.Entities
var linkedChildren = LinkedChildren;
foreach (var i in linkedChildren)
{
if (i.ItemId.HasValue && i.ItemId.Value == itemId)
if (i.ItemId.HasValue)
{
return true;
if (i.ItemId.Value == itemId)
{
return true;
}
continue;
}
var child = GetLinkedChild(i);

View File

@@ -48,7 +48,7 @@ namespace MediaBrowser.Controller
/// <param name="itemId">The item id.</param>
/// <param name="client">The client string.</param>
/// <returns>The dictionary of custom item display preferences.</returns>
IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client);
Dictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client);
/// <summary>
/// Sets the custom item display preference for the user and client.

View File

@@ -55,7 +55,7 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the configured published server url.
/// </summary>
Uri PublishedServerUrl { get; }
string PublishedServerUrl { get; }
/// <summary>
/// Gets the system info.

View File

@@ -466,6 +466,15 @@ namespace MediaBrowser.Controller.Library
/// <param name="people">The people.</param>
void UpdatePeople(BaseItem item, List<PersonInfo> people);
/// <summary>
/// Asynchronously updates the people.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="people">The people.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The async task.</returns>
Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken);
/// <summary>
/// Gets the item ids.
/// </summary>

View File

@@ -10,6 +10,10 @@ namespace MediaBrowser.Controller.Library
{
public static class NameExtensions
{
public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
=> names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First());
private static string RemoveDiacritics(string? name)
{
if (name == null)
@@ -19,9 +23,5 @@ namespace MediaBrowser.Controller.Library
return name.RemoveDiacritics();
}
public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
=> names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First());
}
}

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.7.0</VersionPrefix>
<VersionPrefix>10.7.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -313,6 +313,12 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
// ISO files don't have an ffmpeg format
if (string.Equals(container, "iso", StringComparison.OrdinalIgnoreCase))
{
return null;
}
return container;
}

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