Compare commits

...

147 Commits

Author SHA1 Message Date
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
Joshua M. Boniface
d2851979d4 Fix bad spacer in changelog line 2021-02-28 22:30:24 -05:00
Claus Vium
4b6ff7ffa5 Merge pull request #5312 from BaronGreenback/FixFor5280
(cherry picked from commit 9e77fdc70d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-27 22:30:50 -05:00
Claus Vium
153123278b Merge pull request #5278 from BaronGreenback/STRMFix
Fix for #5168

(cherry picked from commit 64730b5661)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-27 22:30:49 -05:00
Claus Vium
25c19f79d4 Merge pull request #5274 from BaronGreenback/bindfix
(cherry picked from commit 14605280a0)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-27 22:30:49 -05:00
Claus Vium
0f139e8857 Merge pull request #5181 from BaronGreenback/Fix_IPHostIP6Parsing
(cherry picked from commit f8c9c37c29)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-27 22:30:49 -05:00
Claus Vium
e6cc8d5015 Merge pull request #5073 from BaronGreenback/ffmpeg
Fix for 4933: Alternative ffmpeg fix

(cherry picked from commit e5f99762e2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-27 22:30:49 -05:00
Bond-009
ef864e24b9 Merge pull request #5301 from Bond-009/validinput
(cherry picked from commit 5860979500)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-26 21:01:50 -05:00
Joshua M. Boniface
ab054d6239 Merge pull request #5290 from Bond-009/nullref
Fix possible null ref exception

(cherry picked from commit 1442a63556)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-26 21:01:50 -05:00
Bond-009
19ff447e51 Merge pull request #5275 from BaronGreenback/upnpStartupFix
Fixes #5148

(cherry picked from commit 0beda0e32c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-26 21:01:50 -05:00
Joshua M. Boniface
4220808b96 Merge pull request #5270 from Bond-009/imdb
(cherry picked from commit 5ce4df4178)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-26 21:01:50 -05:00
dkanada
eb0621a354 Merge pull request #5217 from jellyfin/auto-manifest
handle plugin manifests automatically

(cherry picked from commit c54ca489f1)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-26 21:01:50 -05:00
Joshua M. Boniface
621c0b9d15 Bump version to 10.7.0~rc4 2021-02-21 13:42:26 -05:00
Andrew Rabert
fecab1d549 Merge pull request #5263 from Bond-009/tmdb
TMDB: Include year in search
(cherry picked from commit 5379fa0bd0)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:30:32 -05:00
Bond-009
bd89cdf8d2 Merge pull request #5255 from cvium/fix_renameuser
(cherry picked from commit ae30eaf320)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:30:32 -05:00
Bond-009
557a091865 Merge pull request #5251 from crobibero/vpp-null-ref
Fix vpp null reference

(cherry picked from commit 467cd9227e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:30:32 -05:00
dkanada
a1773ce97b Merge pull request #5250 from barronpm/user-rename-fix
Fix user renaming logic

(cherry picked from commit b4c2086138)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:30:32 -05:00
Bond-009
be7411dc58 Merge pull request #5230 from orryverducci/double-rate-deint-fix
Fix double rate deinterlacing for some TS files

(cherry picked from commit 32934cb33d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:30:31 -05:00
dkanada
b2a8fd82d8 Merge pull request #5216 from jellyfin/remove-old-settings
remove deprecated settings from server config

(cherry picked from commit 542401c7f4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:30:31 -05:00
Bond-009
d53120602c Merge pull request #5208 from crobibero/api-post-image
Add image file accept to openapi

(cherry picked from commit 76d66e0dee)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:30:31 -05:00
dkanada
da09257d58 Merge pull request #5207 from matthin/default-language
Default to English metadata during the setup wizard.

(cherry picked from commit 75ec8b0c8c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:30:31 -05:00
Joshua M. Boniface
706ac0fafd Merge pull request #5200 from crobibero/dotnet-5.0.3
Update to dotnet 5.0.3

(cherry picked from commit 426b55052f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:30:29 -05:00
Bond-009
ebd4328f02 Merge pull request #5188 from cvium/fix_manifest_bom
Exclude BOM when writing meta.json plugin manifest

(cherry picked from commit fba80cf6f9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Bond-009
39b0d69786 Merge pull request #5171 from Ullmie02/reset-fix
(cherry picked from commit 34e10622c6)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Claus Vium
9f3cebf493 Merge pull request #5154 from crobibero/skip-attributes
(cherry picked from commit 5f63c33557)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Claus Vium
20e985a0d1 Merge pull request #5117 from jellyfin/fix-framerate-locale
Make FRAME-RATE field culture invariant

(cherry picked from commit 63be326302)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Claus Vium
5dbd6f076c Merge pull request #5111 from Larvitar/tmdb-season-name-fix
Remove season name from metadata result

(cherry picked from commit 3fd0987ee3)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Joshua M. Boniface
19a01ccdf3 Merge pull request #5107 from jellyfin/enhanced-nvdec-vpp-tonemap
(cherry picked from commit bd8c269ea2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Bond-009
d816995d27 Merge pull request #5106 from BaronGreenback/FileShareTest2
(cherry picked from commit 28ffbf6945)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Claus Vium
a934477850 Merge pull request #5105 from crobibero/image-null-ref
Add null check for ImageTags

(cherry picked from commit e7e385d7a2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Claus Vium
a7f65bd205 Merge pull request #5099 from crobibero/non-required-query-param
(cherry picked from commit e5828cdbf1)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Claus Vium
8138fc3003 Merge pull request #5095 from Bond-009/sortorder
(cherry picked from commit 98a4e1b840)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Bond-009
46a6cd8d1f Merge pull request #5091 from crobibero/query-param-array
Use ArrayModelBinder for sortBy and sortOrder

(cherry picked from commit b4d04f9ca5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Claus Vium
524df2e45d Merge pull request #5090 from Ullmie02/plugin-startup-fix
(cherry picked from commit f82e6ee8cc)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
dkanada
a486cd27a9 Merge pull request #4935 from ConfusedPolarBear/quickconnect-cleanup
Remove used quick connect tokens

(cherry picked from commit 158e69c6f0)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Bond-009
34053b7259 Merge pull request #4905 from BaronGreenback/streamingHelper
Null exception fix

(cherry picked from commit 9a10a18db1)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-02-21 13:29:02 -05:00
Joshua M. Boniface
d5a7478600 Bump version to 10.7.0-rc3 2021-01-23 15:51:49 -05:00
Joshua M. Boniface
ed333dec43 Merge pull request #5069 from crobibero/obsolete-param
(cherry picked from commit 4b6b90e0b1)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:39:35 -05:00
Joshua M. Boniface
c17c32f9dc Merge pull request #5064 from BaronGreenback/PluginFix
Plugin bug fixes

(cherry picked from commit 3fda50de6d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:39:35 -05:00
Joshua M. Boniface
5cc8ed6516 Merge pull request #5062 from crobibero/delete_log_task
(cherry picked from commit 4d13cad7af)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:39:35 -05:00
Joshua M. Boniface
cdba6b3d35 Merge pull request #5031 from crobibero/5.0.2
Update to dotnet 5.0.2

(cherry picked from commit 3bf7e18886)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:39:32 -05:00
Claus Vium
fa7a8752a9 Merge pull request #5027 from crobibero/episode-first-up
(cherry picked from commit 65c09f82c5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:26 -05:00
Claus Vium
d129afa74e Merge pull request #5025 from BaronGreenback/DlnaFix
(cherry picked from commit b9691e8712)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:26 -05:00
Claus Vium
bc8a1d2276 Merge pull request #4997 from crobibero/subtitle-upload-auth
Require elevated auth to upload subtitles

(cherry picked from commit 5c00b4175a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:26 -05:00
Bond-009
147f9e1edf Merge pull request #4980 from Ullmie02/chinese
Add additional chinese languages

(cherry picked from commit 0bb0dd646f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:26 -05:00
Claus Vium
7796486511 Merge pull request #4978 from BaronGreenback/MultipeProxies
(cherry picked from commit 14bd4a110f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Bond-009
ab5ae34595 Merge pull request #4976 from BaronGreenback/dlnaPortFix
Fixed DLNA Server on RC2

(cherry picked from commit a554423163)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Joshua M. Boniface
81a17b803d Merge pull request #4970 from BaronGreenback/networkTestCorrection
(cherry picked from commit fe9096be94)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Bond-009
cc6afb0971 Merge pull request #4968 from ianjazz246/fix-music-album-display
Fix library with music directly under artist folder

(cherry picked from commit 7758c61ea8)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Bond-009
d9a9a23a3c Merge pull request #4962 from thornbill/fix-playstate-name
Fix capitalization of Playstate message

(cherry picked from commit 07650d91da)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Bond-009
dd1fddf79c Merge pull request #4961 from crobibero/person-blurhash-null-ref
Fix potential null reference

(cherry picked from commit a8230c07ea)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Joshua M. Boniface
4df7522629 Merge pull request #4956 from jceresini/master
Fix rpm package dependencies

(cherry picked from commit 3204ce71b3)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Bond-009
9c83a6cef9 Merge pull request #4936 from crobibero/series-start-item-id
Fix inverted SkipWhile

(cherry picked from commit 841996c642)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
dkanada
910819c71c Merge pull request #4920 from crobibero/person-blurhash
Attach correct Blurhash to BaseItemPerson

(cherry picked from commit c117b12b6b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Bond-009
a0e047d560 Change converter log level (#4916)
Change converter log level

(cherry picked from commit 9265dd68a4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Joshua M. Boniface
34322ba491 Merge pull request #4911 from Ullmie02/nuget_again
(cherry picked from commit c5e9d56028)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Bond-009
801dd74ff6 Merge pull request #4906 from Spacetech/library_scan_ignore_inaccessible
Ignore inaccessible files & folders during library scans

(cherry picked from commit 4549c96f6d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Joshua M. Boniface
129453214f Merge pull request #4859 from Ullmie02/ci
Don't build unstable Nuget packages on tags

(cherry picked from commit 7227d0d48b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
Joshua M. Boniface
ac82fead82 Bump version to 10.7.0-rc2 2020-12-31 19:23:19 -05:00
dkanada
9a59ff3c87 Merge pull request #4902 from BaronGreenback/NetmaskFix
Fixed loopback subnet

(cherry picked from commit d6db0e4b02)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:50:02 -05:00
Joshua M. Boniface
a16cf8ec0a Merge pull request #4890 from nielsvanvelzen/4888-fix-search-hints
Fix search hint endpoint error

(cherry picked from commit 7caba04c3c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:50:02 -05:00
Joshua M. Boniface
4a2b143028 Merge pull request #4884 from crobibero/json-nullable-guid-converter
Add JsonConverter for Nullable Guids

(cherry picked from commit 0de45d8724)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:50:02 -05:00
Joshua M. Boniface
d737c2b84a Merge pull request #4729 from BaronGreenback/19.0RC---Fix-networkInterfaceFix
(cherry picked from commit ccc1b8bf92)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:50:02 -05:00
Joshua M. Boniface
1ad8e54035 Merge pull request #4709 from BaronGreenback/PluginDowngrade
(cherry picked from commit 406ae3e43a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:49:53 -05:00
Joshua M. Boniface
83dd3e2201 Merge pull request #4891 from Artiume/patch-1
(cherry picked from commit eb084f9021)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 19:13:04 -05:00
Bond-009
d9634b7fc0 Merge pull request #4874 from MrTimscampi/enable-tmdb-omdb
Enable TMDB and OMDB by default

(cherry picked from commit f8681aa518)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Bond-009
05b34b2710 Merge pull request #4872 from BaronGreenback/NetworkManagerFix
Removed workaround code as web is now fixed.

(cherry picked from commit 4ed20c75f7)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
2dab55a8f2 Merge pull request #4863 from nyanmisaka/boxes-backdrop
Fix boxes in library name backdrop

(cherry picked from commit f2e05bd183)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
124ab090bc Merge pull request #4861 from crobibero/null-ref
Fix null reference when logging

(cherry picked from commit 93b754c366)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
dkanada
03c8216946 Merge pull request #4860 from nyanmisaka/3ch-transcode-hls
Avoid transcoding to 3ch audio for HLS streaming

(cherry picked from commit 2913e2604c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
f72f27ff45 Merge pull request #4856 from nyanmisaka/amf-profile
Fix some profiles for H264 AMF encoder

(cherry picked from commit afdc98746b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Bond-009
71188ad27a Merge pull request #4855 from crobibero/cache-json-serializer
Initialize JsonSerializerOptions statically

(cherry picked from commit f42208673f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
0d9e8b4f00 Merge pull request #4852 from ryanpetris/fix-schedulesdirect-refresh
SchedulesDirect no longer refreshes channels properly

(cherry picked from commit e36881f4fd)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
fbfb23abab Merge pull request #4850 from BaronGreenback/NetworkApiFix
Null reference fix

(cherry picked from commit bfdd4727b5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
622c71ce1c Merge pull request #4847 from crobibero/display-prefs-migration-x251256
Fix another key collision in MigrateDisplayPreferencesDatabase

(cherry picked from commit ca535b2fbe)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
dbc9256945 Merge pull request #4842 from crobibero/date-time-formatter
Add JsonDateTimeConverter

(cherry picked from commit c8a89b0fe3)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
6b20aaaa6a Merge pull request #4836 from crobibero/dashboard-theme
Return dashboardTheme when requesting DisplayPreferences

(cherry picked from commit 98da2c67a5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
c2097ba5fe Merge pull request #4833 from Ullmie02/similar-fix
Fix similar items endpoint for movies and TV

(cherry picked from commit c90c382e2f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Bond-009
8884f4f288 Merge pull request #4828 from joshuaboniface/linux-alt-arch
(cherry picked from commit 2e8e96874f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
ce741f541c Merge pull request #4824 from crobibero/livestream-post-body
Add request parameters to OpenLiveStreamDto

(cherry picked from commit 53119ed2a1)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
783d6409af Merge pull request #4821 from BaronGreenback/disableDlna
(cherry picked from commit a77788906c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
5bf25ce2cc Merge pull request #4819 from crobibero/download-name
Set filename when downloading file

(cherry picked from commit 668e168c47)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
6c2ddd9758 Merge pull request #4816 from nyanmisaka/profiles
Fix some video profiles for Android client

(cherry picked from commit e9db47cd20)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
8760a298b1 Merge pull request #4807 from nyanmisaka/ps4-dlna
Correct DLNA audio codecs for PS3 and PS4

(cherry picked from commit 1f2488d7d9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
c08933c9d9 Merge pull request #4803 from ryanpetris/fix-getuser
Fix Live TV Recording Scheduling

(cherry picked from commit 6274cf8fcc)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Bond-009
c86c652006 Merge pull request #4794 from cvium/fix_image_upload
Convert from base64 when saving item images

(cherry picked from commit e84622b4ab)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Bond-009
c08ce82a04 Merge pull request #4792 from cvium/fix_missing_seasons
Add missing seasons during AfterMetadataRefresh

(cherry picked from commit 8eeed82523)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
b10178bd1e Merge pull request #4789 from crobibero/provider-search
Fix get provider id extension

(cherry picked from commit bc8f1bdcac)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Bond-009
00a608dfab Merge pull request #4781 from crobibero/map-xmltv
Use request body for mapping xml channels

(cherry picked from commit 906ee4f962)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
480ded0671 Merge pull request #4771 from crobibero/typed-get-preference
Use typed UserManager GetPreference

(cherry picked from commit 21d2e9ff0c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Bond-009
98c081f0ce Merge pull request #4774 from nyanmisaka/finetune-tonemap
Fine tune some tone mapping params

(cherry picked from commit 381341c83b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Joshua M. Boniface
e77d0ab5fd Merge pull request #4773 from Artiume/patch-10
Remove opf extension for book types

(cherry picked from commit e7ae712437)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
28ef1bc8b2 Merge pull request #4769 from crobibero/typo
Check correct fetcher list for provider name

(cherry picked from commit 4ecb30ef10)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
7ebf7014e0 Merge pull request #4767 from nyanmisaka/fix-ssl-save
(cherry picked from commit f9a78625b7)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
89a649cc23 Merge pull request #4762 from crobibero/api-file-return
Fix openapi file schema

(cherry picked from commit b56feac841)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
24b2991def Merge pull request #4761 from crobibero/playlist
(cherry picked from commit 13bb5e1ead)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
87dde66e92 Merge pull request #4758 from nyanmisaka/fix-landingScreen-options
(cherry picked from commit f8ef38c0ea)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
1b82ef905e Merge pull request #4757 from cvium/semi_revert_defer_image_loading
(cherry picked from commit 0cddfbcff5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
02cc83b807 Merge pull request #4756 from crobibero/api-key-inverted-condition
Fix inverted condition when authenticating with an ApiKey

(cherry picked from commit c1bb29532f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
0d8fa795a0 Merge pull request #4753 from crobibero/5.0.1
Update to dotnet 5.0.1

(cherry picked from commit a56ac65449)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
b32f15ab1e Merge pull request #4751 from nyanmisaka/mpegts-batchsize
(cherry picked from commit 9e601ba731)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Joshua M. Boniface
26993e39f7 Merge pull request #4750 from crobibero/skia
Fix blueberry

(cherry picked from commit 7b5319bb07)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
ddedb2d7f1 Merge pull request #4749 from crobibero/guid-standard
(cherry picked from commit 933e7fa159)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
5c0d930dc3 Merge pull request #4743 from crobibero/metadata-options
Actually use library options when filtering metadata providers

(cherry picked from commit e85884ddb9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
13d62c5977 Merge pull request #4741 from jellyfin/tests8
Add tests for HdHomerunHost.GetLineup

(cherry picked from commit 31e8273795)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
9799b6ae81 Merge pull request #4738 from jellyfin/tests8
Add tests for HdHomerunHost.GetModelInfo

(cherry picked from commit e6650651b3)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
c1dd8f2050 Merge pull request #4737 from crobibero/missing-ensure-success
(cherry picked from commit f322866127)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
853c328763 Merge pull request #4736 from nyanmisaka/fix-custom-order
Fix custom library order

(cherry picked from commit b83bc0a589)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Anthony Lavado
126753a1fe Merge pull request #4735 from crobibero/json-recursion
Fix JsonConverter recursion

(cherry picked from commit 94d805d03d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
24e4fcc3b7 Merge pull request #4733 from crobibero/omdb-null
Fix potential null reference in OMDB

(cherry picked from commit 87e13b858a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
aae90a8480 Merge pull request #4730 from crobibero/base-item-dto-guid-nullable
Don't serialize empty GUID to null

(cherry picked from commit b6ecaccf92)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
11a37884f0 Merge pull request #4726 from BaronGreenback/19.0RC--Fix-CertificateLoadError
Fix - Access Denied on using certificates in windows as user.

(cherry picked from commit c5a5f77b9e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
49f3579c1b Merge pull request #4724 from BaronGreenback/17.0RC----DLNA_PlayTo_Fix
(cherry picked from commit e6ea5a776d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
259d811b95 Merge pull request #4722 from crobibero/forbid
Fix API forbidden response

(cherry picked from commit 80ff564143)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Joshua M. Boniface
7e01cce884 Merge pull request #4716 from OancaAndrei/syncplay-new-auth-policies
(cherry picked from commit 4f6a585424)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
2e5333c1d4 Merge pull request #4715 from crobibero/hdhr-json-bool
Add number to bool json converter

(cherry picked from commit 0aad17554c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
e8e1bbffd9 Merge pull request #4713 from crobibero/robots
(cherry picked from commit a3a7467f49)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
723fe43d2e Merge pull request #4711 from barronpm/api-fixes
Add required attributes to parameters

(cherry picked from commit 4c199ac9a5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
e70a6d41f4 Merge pull request #4710 from OancaAndrei/syncplay-fix-session-restore
Restore sessions in SyncPlay groups upon reconnection

(cherry picked from commit a57e465de9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
9d4417eee3 Merge pull request #4706 from cvium/fix_aspectratio_again
Only apply series image aspect ratio if episode/season has no primary image

(cherry picked from commit 0d5f651ae9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
67f41386ba Merge pull request #4701 from crobibero/plugin-version
(cherry picked from commit 7455de1f85)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
b1af8db423 Merge pull request #4699 from crobibero/display_prefs_index
Fix CustomItemDisplayPreferences unique key collision in the migration

(cherry picked from commit b3caa51173)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
91656acabb Merge pull request #4678 from BaronGreenback/network_routing_fix_17.0rc
Change logging level and message in NetworkManager

(cherry picked from commit 885d42cb65)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
5fa8c83ba4 Merge pull request #4675 from BaronGreenback/ProxyDNS
(cherry picked from commit 8c00fbea9c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
683bc27b27 Merge pull request #4672 from cvium/fix_mergeversions_which_was_unrelated_to_my_bughunt
Fix MergeVersions endpoint

(cherry picked from commit 26919eed26)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:31 -05:00
Bond-009
0b6a05cf82 Merge pull request #4671 from cvium/allow_proxy
Clear KnownNetworks and KnownProxies if none are configured explicitly

(cherry picked from commit 804dd00425)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:31 -05:00
Joshua M. Boniface
2647935b96 Merge pull request #4669 from MrTimscampi/fix-npm-public
Fix NPM command in CI

(cherry picked from commit 8976b22ac4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-05 01:36:00 -05:00
Joshua M. Boniface
2a4023c6c7 Merge pull request #4667 from joshuaboniface/fix-nuget-ci
Remove obsolete erroring command

(cherry picked from commit f2c2beca0f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-05 01:35:18 -05:00
Joshua M. Boniface
2a2630098b Merge pull request #4662 from joshuaboniface/fix-bump-version
Fix bad do in bump_version

(cherry picked from commit e3a1991fe9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-05 01:12:47 -05:00
Joshua M. Boniface
79472dce70 Bump version to 10.7.0~rc1 2020-12-04 21:03:57 -05:00
262 changed files with 6218 additions and 2572 deletions

View File

@@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest" default: "ubuntu-latest"
- name: DotNetSdkVersion - name: DotNetSdkVersion
type: string type: string
default: 5.0.100 default: 5.0.103
jobs: jobs:
- job: CompatibilityCheck - job: CompatibilityCheck

View File

@@ -4,7 +4,7 @@
default: "ubuntu-latest" default: "ubuntu-latest"
- name: GeneratorVersion - name: GeneratorVersion
type: string type: string
default: "5.0.0-beta2" default: "5.0.1"
jobs: jobs:
- job: GenerateApiClients - job: GenerateApiClients
@@ -52,7 +52,8 @@ jobs:
- task: Npm@1 - task: Npm@1
displayName: 'Publish stable typescript axios client' displayName: 'Publish stable typescript axios client'
inputs: inputs:
command: publish command: custom
customCommand: publish --access public
publishRegistry: useExternalRegistry publishRegistry: useExternalRegistry
publishEndpoint: 'jellyfin-bot for NPM' publishEndpoint: 'jellyfin-bot for NPM'
workingDir: ./apiclient/generated/typescript/axios workingDir: ./apiclient/generated/typescript/axios

View File

@@ -1,7 +1,7 @@
parameters: parameters:
LinuxImage: 'ubuntu-latest' LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj' RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 5.0.100 DotNetSdkVersion: 5.0.103
jobs: jobs:
- job: Build - job: Build

View File

@@ -22,6 +22,12 @@ jobs:
BuildConfiguration: ubuntu.armhf BuildConfiguration: ubuntu.armhf
Linux.amd64: Linux.amd64:
BuildConfiguration: linux.amd64 BuildConfiguration: linux.amd64
Linux.amd64-musl:
BuildConfiguration: linux.amd64-musl
Linux.arm64:
BuildConfiguration: linux.arm64
Linux.armhf:
BuildConfiguration: linux.armhf
Windows.amd64: Windows.amd64:
BuildConfiguration: windows.amd64 BuildConfiguration: windows.amd64
MacOS: MacOS:
@@ -187,6 +193,10 @@ jobs:
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
variables:
- name: JellyfinVersion
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET 5.0 sdk' displayName: 'Use .NET 5.0 sdk'
@@ -198,12 +208,19 @@ jobs:
displayName: 'Build Stable Nuget packages' displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs: inputs:
command: 'pack' command: 'custom'
packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj' projects: |
versioningScheme: 'off' Jellyfin.Data/Jellyfin.Data.csproj
MediaBrowser.Common/MediaBrowser.Common.csproj
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
custom: 'pack'
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Build Unstable Nuget packages' displayName: 'Build Unstable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs: inputs:
command: 'custom' command: 'custom'
projects: | projects: |
@@ -221,18 +238,12 @@ jobs:
pathToPublish: $(Build.ArtifactStagingDirectory) pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: Jellyfin Nuget Packages artifactName: Jellyfin Nuget Packages
- task: NuGetAuthenticate@0
displayName: 'Authenticate to stable Nuget feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
nuGetServiceConnections: 'NugetOrg'
- task: NuGetCommand@2 - task: NuGetCommand@2
displayName: 'Push Nuget packages to stable feed' displayName: 'Push Nuget packages to stable feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs: inputs:
command: 'push' command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg' packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
nuGetFeedType: 'external' nuGetFeedType: 'external'
publishFeedCredentials: 'NugetOrg' publishFeedCredentials: 'NugetOrg'
allowPackageConflicts: true # This ignores an error if the version already exists allowPackageConflicts: true # This ignores an error if the version already exists

View File

@@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj" default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion - name: DotNetSdkVersion
type: string type: string
default: 5.0.100 default: 5.0.103
jobs: jobs:
- job: Test - job: Test

View File

@@ -6,7 +6,7 @@ variables:
- name: RestoreBuildProjects - name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj' value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion - name: DotNetSdkVersion
value: 5.0.100 value: 5.0.103
pr: pr:
autoCancel: true autoCancel: true

View File

@@ -24,7 +24,7 @@ jobs:
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: '5.0.100' dotnet-version: '5.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1
with: with:

View File

@@ -49,6 +49,7 @@
- [h1nk](https://github.com/h1nk) - [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93) - [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017) - [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga) - [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h) - [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface) - [joshuaboniface](https://github.com/joshuaboniface)
@@ -141,6 +142,7 @@
- [Pusta](https://github.com/pusta) - [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen) - [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk) - [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
# Emby Contributors # Emby Contributors

View File

@@ -96,6 +96,7 @@ namespace Emby.Dlna.Didl
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8)) using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
{ {
// If this using are changed to single lines, then write.Flush needs to be appended before the return.
using (var writer = XmlWriter.Create(builder, settings)) using (var writer = XmlWriter.Create(builder, settings))
{ {
// writer.WriteStartDocument(); // writer.WriteStartDocument();

View File

@@ -395,7 +395,8 @@ namespace Emby.Dlna
{ {
Directory.CreateDirectory(systemProfilesPath); 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); await stream.CopyToAsync(fileStream).ConfigureAwait(false);
} }

View File

@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.PlayTo; using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager; using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@@ -52,6 +53,8 @@ namespace Emby.Dlna.Main
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object(); private readonly object _syncLock = new object();
private readonly NetworkConfiguration _netConfig;
private readonly bool _disabled;
private PlayToManager _manager; private PlayToManager _manager;
private SsdpDevicePublisher _publisher; private SsdpDevicePublisher _publisher;
@@ -122,10 +125,22 @@ namespace Emby.Dlna.Main
httpClientFactory, httpClientFactory,
config); config);
Current = this; Current = this;
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
if (_disabled)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
} }
public static DlnaEntryPoint Current { get; private set; } public static DlnaEntryPoint Current { get; private set; }
/// <summary>
/// Gets a value indicating whether the dlna server is enabled.
/// </summary>
public static bool Enabled { get; private set; }
public IContentDirectory ContentDirectory { get; private set; } public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; } public IConnectionManager ConnectionManager { get; private set; }
@@ -136,6 +151,12 @@ namespace Emby.Dlna.Main
{ {
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
if (_disabled)
{
// No use starting as dlna won't work, as we're running purely on HTTPS.
return;
}
ReloadComponents(); ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
@@ -152,6 +173,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents() private void ReloadComponents()
{ {
var options = _config.GetDlnaConfiguration(); var options = _config.GetDlnaConfiguration();
Enabled = options.EnableServer;
StartSsdpHandler(); StartSsdpHandler();
@@ -206,7 +228,10 @@ namespace Emby.Dlna.Main
{ {
try try
{ {
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer); if (communicationsServer != null)
{
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -290,12 +315,18 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
{
// DLNA will only work over http, so we must reset to http:// : {port}.
uri.Scheme = "http";
uri.Port = _netConfig.HttpServerPortNumber;
}
var device = new SsdpRootDevice var device = new SsdpRootDevice
{ {
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri, // Must point to the URL that serves your devices UPnP description document. Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
Address = address.Address, Address = address.Address,
PrefixLength = address.PrefixLength, PrefixLength = address.PrefixLength,
FriendlyName = "Jellyfin", FriendlyName = "Jellyfin",

View File

@@ -132,7 +132,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e) private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{ {
if (_disposed) if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{ {
return; return;
} }
@@ -340,10 +340,19 @@ namespace Emby.Dlna.PlayTo
} }
var playlist = new PlaylistItem[len]; var playlist = new PlaylistItem[len];
playlist[0] = CreatePlaylistItem(items[0], user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex);
// Not nullable enabled - so this is required.
playlist[0] = CreatePlaylistItem(
items[0],
user,
command.StartPositionTicks ?? 0,
command.MediaSourceId ?? string.Empty,
command.AudioStreamIndex,
command.SubtitleStreamIndex);
for (int i = 1; i < len; i++) for (int i = 1; i < len; i++)
{ {
playlist[i] = CreatePlaylistItem(items[i], user, 0, null, null, null); playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
} }
_logger.LogDebug("{0} - Playlist created", _session.DeviceName); _logger.LogDebug("{0} - Playlist created", _session.DeviceName);
@@ -817,7 +826,7 @@ namespace Emby.Dlna.PlayTo
return SendPlayCommand(data as PlayRequest, cancellationToken); return SendPlayCommand(data as PlayRequest, cancellationToken);
} }
if (name == SessionMessageType.PlayState) if (name == SessionMessageType.Playstate)
{ {
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
} }
@@ -887,16 +896,16 @@ namespace Emby.Dlna.PlayTo
var parts = url.Split('/'); var parts = url.Split('/');
for (var i = 0; i < parts.Length; i++) for (var i = 0; i < parts.Length - 1; i++)
{ {
var part = parts[i]; var part = parts[i];
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) || if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase)) string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{ {
if (parts.Length > i + 1) if (Guid.TryParse(parts[i + 1], out var result))
{ {
return Guid.Parse(parts[i + 1]); return result;
} }
} }
} }

View File

@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts", Container = "ts,mpegts",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264", VideoCodec = "mpeg1video,mpeg2video,h264",
AudioCodec = "ac3,mp2,mp3,aac" AudioCodec = "aac,ac3,mp2"
}, },
new DirectPlayProfile new DirectPlayProfile
{ {
@@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
{ {
Container = "ts", Container = "ts",
VideoCodec = "h264", VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3", AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new TranscodingProfile new TranscodingProfile

View File

@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts", Container = "ts,mpegts",
Type = DlnaProfileType.Video, Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264", VideoCodec = "mpeg1video,mpeg2video,h264",
AudioCodec = "ac3,mp2,mp3,aac" AudioCodec = "aac,ac3,mp2"
}, },
new DirectPlayProfile new DirectPlayProfile
{ {
@@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
{ {
Container = "ts", Container = "ts",
VideoCodec = "h264", VideoCodec = "h264",
AudioCodec = "mp3", AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video Type = DlnaProfileType.Video
}, },
new TranscodingProfile new TranscodingProfile

View File

@@ -38,7 +38,7 @@
<XmlRootAttributes /> <XmlRootAttributes />
<DirectPlayProfiles> <DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@@ -46,7 +46,7 @@
</DirectPlayProfiles> </DirectPlayProfiles>
<TranscodingProfiles> <TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles> </TranscodingProfiles>
<ContainerProfiles> <ContainerProfiles>

View File

@@ -38,7 +38,7 @@
<XmlRootAttributes /> <XmlRootAttributes />
<DirectPlayProfiles> <DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" /> <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" /> <DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" /> <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" /> <DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" /> <DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@@ -46,7 +46,7 @@
</DirectPlayProfiles> </DirectPlayProfiles>
<TranscodingProfiles> <TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" /> <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles> </TranscodingProfiles>
<ContainerProfiles> <ContainerProfiles>

View File

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

View File

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

View File

@@ -53,7 +53,8 @@ namespace Emby.Server.Implementations.AppBase
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
// Save it after load in case we got new items // 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); fs.Write(newBytes, 0, newBytesLen);
} }

View File

@@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@@ -42,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session; using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV; using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Configuration;
@@ -97,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime; using Prometheus.DotNetRuntime;
@@ -116,10 +119,13 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager; private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions; private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder; private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private string[] _urlPrefixes; private string[] _urlPrefixes;
@@ -181,16 +187,6 @@ namespace Emby.Server.Implementations
protected IServiceCollection ServiceCollection { get; } protected IServiceCollection ServiceCollection { get; }
private IPlugin[] _plugins;
private IReadOnlyList<LocalPlugin> _pluginsManifests;
/// <summary>
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
public IReadOnlyList<IPlugin> Plugins => _plugins;
/// <summary> /// <summary>
/// Gets the logger factory. /// Gets the logger factory.
/// </summary> /// </summary>
@@ -234,6 +230,11 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
public int HttpsPort { get; private set; } public int HttpsPort { get; private set; }
/// <summary>
/// Gets the value of the PublishedServerUrl setting.
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <summary> /// <summary>
/// Gets the server configuration manager. /// Gets the server configuration manager.
/// </summary> /// </summary>
@@ -246,12 +247,14 @@ namespace Emby.Server.Implementations
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> 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="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="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost( public ApplicationHost(
IServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStartupOptions options, IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem, IFileSystem fileSystem,
IServiceCollection serviceCollection) IServiceCollection serviceCollection)
{ {
@@ -275,6 +278,7 @@ namespace Emby.Server.Implementations
Logger = LoggerFactory.CreateLogger<ApplicationHost>(); Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_startupOptions = options; _startupOptions = options;
_startupConfig = startupConfig;
// Initialize runtime stat collection // Initialize runtime stat collection
if (ServerConfigurationManager.Configuration.EnableMetrics) if (ServerConfigurationManager.Configuration.EnableMetrics)
@@ -284,16 +288,16 @@ namespace Emby.Server.Implementations
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
CertificateInfo = new CertificateInfo
{
Path = ServerConfigurationManager.Configuration.CertificatePath,
Password = ServerConfigurationManager.Configuration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ServerConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
} }
/// <summary> /// <summary>
@@ -393,16 +397,41 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns> /// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type) protected object CreateInstanceSafe(Type type)
{ {
if (_creatingInstances == null)
{
_creatingInstances = new List<Type>();
}
if (_creatingInstances.IndexOf(type) != -1)
{
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances)
{
Logger.LogError("Called from: {TypeName}", entry.FullName);
}
_pluginManager.FailPlugin(type.Assembly);
throw new ExternalException("DI Loop detected.");
}
try try
{ {
_creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type); Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type); return ActivatorUtilities.CreateInstance(ServiceProvider, type);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error creating {Type}", type); Logger.LogError(ex, "Error creating {Type}", type);
// If this is a plugin fail it.
_pluginManager.FailPlugin(type.Assembly);
return null; return null;
} }
finally
{
_creatingInstances.Remove(type);
}
} }
/// <summary> /// <summary>
@@ -412,11 +441,7 @@ namespace Emby.Server.Implementations
/// <returns>``0.</returns> /// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>(); public T Resolve<T>() => ServiceProvider.GetService<T>();
/// <summary> /// <inheritdoc/>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
public IEnumerable<Type> GetExportTypes<T>() public IEnumerable<Type> GetExportTypes<T>()
{ {
var currentType = typeof(T); var currentType = typeof(T);
@@ -445,6 +470,27 @@ namespace Emby.Server.Implementations
return parts; return parts;
} }
/// <inheritdoc />
public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
{
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes<T>()
.Select(i => defaultFunc(i))
.Where(i => i != null)
.Cast<T>()
.ToList();
if (manageLifetime)
{
lock (_disposableParts)
{
_disposableParts.AddRange(parts.OfType<IDisposable>());
}
}
return parts;
}
/// <summary> /// <summary>
/// Runs the startup tasks. /// Runs the startup tasks.
/// </summary> /// </summary>
@@ -456,6 +502,7 @@ namespace Emby.Server.Implementations
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
_mediaEncoder.SetFFmpegPath(); _mediaEncoder.SetFFmpegPath();
@@ -505,11 +552,18 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort; HttpsPort = NetworkConfiguration.DefaultHttpsPort;
} }
CertificateInfo = new CertificateInfo
{
Path = networkConfiguration.CertificatePath,
Password = networkConfiguration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
DiscoverTypes(); DiscoverTypes();
RegisterServices(); RegisterServices();
RegisterPluginServices(); _pluginManager.RegisterServices(ServiceCollection);
} }
/// <summary> /// <summary>
@@ -523,7 +577,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(ConfigurationManager); ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this); ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>(); ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
@@ -714,7 +768,7 @@ namespace Emby.Server.Implementations
// Don't use an empty string password // Don't use an empty string password
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password; var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
var localCert = new X509Certificate2(certificateLocation, password); var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; // localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
if (!localCert.HasPrivateKey) if (!localCert.HasPrivateKey)
{ {
@@ -768,34 +822,7 @@ namespace Emby.Server.Implementations
} }
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
_plugins = GetExports<IPlugin>() _pluginManager.CreatePlugins();
.Where(i => i != null)
.ToArray();
if (Plugins != null)
{
foreach (var plugin in Plugins)
{
if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
{
// Ensure the version number matches the Plugin Manifest information.
foreach (var item in _pluginsManifests)
{
if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase))
{
// Update version number to that of the manifest.
assemblyPlugin.SetAttributes(
plugin.AssemblyFilePath,
Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)),
item.Version);
break;
}
}
}
Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
}
}
_urlPrefixes = GetUrlPrefixes().ToArray(); _urlPrefixes = GetUrlPrefixes().ToArray();
@@ -834,22 +861,6 @@ namespace Emby.Server.Implementations
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
} }
private void RegisterPluginServices()
{
foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
{
try
{
var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
instance.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
}
}
}
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies) private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{ {
foreach (var ass in assemblies) foreach (var ass in assemblies)
@@ -862,11 +873,13 @@ namespace Emby.Server.Implementations
catch (FileNotFoundException ex) catch (FileNotFoundException ex)
{ {
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
_pluginManager.FailPlugin(ass);
continue; continue;
} }
catch (TypeLoadException ex) catch (TypeLoadException ex)
{ {
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName); Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
_pluginManager.FailPlugin(ass);
continue; continue;
} }
@@ -912,11 +925,11 @@ namespace Emby.Server.Implementations
protected void OnConfigurationUpdated(object sender, EventArgs e) protected void OnConfigurationUpdated(object sender, EventArgs e)
{ {
var requiresRestart = false; var requiresRestart = false;
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Don't do anything if these haven't been set yet // Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0) if (HttpPort != 0 && HttpsPort != 0)
{ {
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Need to restart if ports have changed // Need to restart if ports have changed
if (networkConfiguration.HttpServerPortNumber != HttpPort || if (networkConfiguration.HttpServerPortNumber != HttpPort ||
networkConfiguration.HttpsPortNumber != HttpsPort) networkConfiguration.HttpsPortNumber != HttpsPort)
@@ -936,10 +949,7 @@ namespace Emby.Server.Implementations
requiresRestart = true; requiresRestart = true;
} }
var currentCertPath = CertificateInfo?.Path; if (ValidateSslCertificate(networkConfiguration))
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
{ {
requiresRestart = true; requiresRestart = true;
} }
@@ -952,6 +962,33 @@ namespace Emby.Server.Implementations
} }
} }
/// <summary>
/// Validates the SSL certificate.
/// </summary>
/// <param name="networkConfig">The new configuration.</param>
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
private bool ValidateSslCertificate(NetworkConfiguration networkConfig)
{
var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
{
if (File.Exists(newPath))
{
return true;
}
throw new FileNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"Certificate file '{0}' does not exist.",
newPath));
}
return false;
}
/// <summary> /// <summary>
/// Notifies that the kernel that a change has been made that requires a restart. /// Notifies that the kernel that a change has been made that requires a restart.
/// </summary> /// </summary>
@@ -1005,129 +1042,15 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal(); protected abstract void RestartInternal();
/// <inheritdoc/>
public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
{
var minimumVersion = new Version(0, 0, 0, 1);
var versions = new List<LocalPlugin>();
if (!Directory.Exists(path))
{
// Plugin path doesn't exist, don't try to enumerate subfolders.
return Enumerable.Empty<LocalPlugin>();
}
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
try
{
var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
targetAbi = minimumVersion;
}
if (!Version.TryParse(manifest.Version, out var version))
{
version = minimumVersion;
}
if (ApplicationVersion >= targetAbi)
{
// Only load Plugins if the plugin is built for this version or below.
versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
}
}
else
{
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
{
// Versioned folder.
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
}
else
{
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
}
}
}
catch
{
continue;
}
}
string lastName = string.Empty;
versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
{
versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
lastName = versions[x].Name;
continue;
}
if (!string.IsNullOrEmpty(lastName) && cleanup)
{
// Attempt a cleanup of old folders.
try
{
Logger.LogDebug("Deleting {Path}", versions[x].Path);
Directory.Delete(versions[x].Path, true);
}
catch (Exception e)
{
Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
}
versions.RemoveAt(x);
}
}
return versions;
}
/// <summary> /// <summary>
/// Gets the composable part assemblies. /// Gets the composable part assemblies.
/// </summary> /// </summary>
/// <returns>IEnumerable{Assembly}.</returns> /// <returns>IEnumerable{Assembly}.</returns>
protected IEnumerable<Assembly> GetComposablePartAssemblies() protected IEnumerable<Assembly> GetComposablePartAssemblies()
{ {
if (Directory.Exists(ApplicationPaths.PluginsPath)) foreach (var p in _pluginManager.LoadAssemblies())
{ {
_pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList(); yield return p;
foreach (var plugin in _pluginsManifests)
{
foreach (var file in plugin.DllFiles)
{
Assembly plugAss;
try
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss;
}
}
} }
// Include composable parts in the Model assembly // Include composable parts in the Model assembly
@@ -1236,10 +1159,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null) public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{ {
// Published server ends with a / // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null) if (!string.IsNullOrEmpty(PublishedServerUrl))
{ {
// Published server ends with a '/', so we need to remove it. // 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); string smart = NetManager.GetBindInterface(ipAddress, out port);
@@ -1256,10 +1179,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(HttpRequest request, int? port = null) public string GetSmartApiUrl(HttpRequest request, int? port = null)
{ {
// Published server ends with a / // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null) if (!string.IsNullOrEmpty(PublishedServerUrl))
{ {
// Published server ends with a '/', so we need to remove it. // 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); string smart = NetManager.GetBindInterface(request, out port);
@@ -1276,10 +1199,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(string hostname, int? port = null) public string GetSmartApiUrl(string hostname, int? port = null)
{ {
// Published server ends with a / // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null) if (!string.IsNullOrEmpty(PublishedServerUrl))
{ {
// Published server ends with a '/', so we need to remove it. // 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); string smart = NetManager.GetBindInterface(hostname, out port);
@@ -1369,17 +1292,6 @@ namespace Emby.Server.Implementations
} }
} }
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
public void RemovePlugin(IPlugin plugin)
{
var list = _plugins.ToList();
list.Remove(plugin);
_plugins = list.ToArray();
}
public IEnumerable<Assembly> GetApiPluginAssemblies() public IEnumerable<Assembly> GetApiPluginAssemblies()
{ {
var assemblies = _allConcreteTypes var assemblies = _allConcreteTypes

View File

@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Collections
var name = _localizationManager.GetLocalizedString("Collections"); var name = _localizationManager.GetLocalizedString("Collections");
await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false); await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
return FindFolders(path).First(); return FindFolders(path).First();
} }

View File

@@ -88,38 +88,12 @@ namespace Emby.Server.Implementations.Configuration
var newConfig = (ServerConfiguration)newConfiguration; var newConfig = (ServerConfiguration)newConfiguration;
ValidateMetadataPath(newConfig); ValidateMetadataPath(newConfig);
ValidateSslCertificate(newConfig);
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig)); ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
base.ReplaceConfiguration(newConfiguration); base.ReplaceConfiguration(newConfiguration);
} }
/// <summary>
/// Validates the SSL certificate.
/// </summary>
/// <param name="newConfig">The new configuration.</param>
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
private void ValidateSslCertificate(BaseApplicationConfiguration newConfig)
{
var serverConfig = (ServerConfiguration)newConfig;
var newPath = serverConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
{
if (!File.Exists(newPath))
{
throw new FileNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"Certificate file '{0}' does not exist.",
newPath));
}
}
}
/// <summary> /// <summary>
/// Validates the metadata path. /// Validates the metadata path.
/// </summary> /// </summary>

View File

@@ -582,7 +582,26 @@ namespace Emby.Server.Implementations.Dto
{ {
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary); baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture); baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
baseItemPerson.ImageBlurHashes = dto.ImageBlurHashes; if (dto.ImageBlurHashes != null)
{
// Only add BlurHash for the person's image.
baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
{
if (blurHash != null)
{
baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
foreach (var (imageId, blurHashValue) in blurHash)
{
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
{
baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
}
}
}
}
}
list.Add(baseItemPerson); list.Add(baseItemPerson);
} }
} }
@@ -1138,7 +1157,10 @@ namespace Emby.Server.Implementations.Dto
if (episodeSeries != null) if (episodeSeries != null)
{ {
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary); dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
AttachPrimaryImageAspectRatio(dto, episodeSeries); if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, episodeSeries);
}
} }
} }
@@ -1185,7 +1207,10 @@ namespace Emby.Server.Implementations.Dto
if (series != null) if (series != null)
{ {
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary); dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
AttachPrimaryImageAspectRatio(dto, series); if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, series);
}
} }
} }
} }

View File

@@ -31,7 +31,7 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
@@ -74,5 +74,4 @@
<EmbeddedResource Include="Localization\Core\*.json" /> <EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.csv" /> <EmbeddedResource Include="Localization\Ratings\*.csv" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,3 +1,5 @@
#nullable enable
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -29,7 +31,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary> /// <summary>
/// The UDP server. /// The UDP server.
/// </summary> /// </summary>
private UdpServer _udpServer; private UdpServer? _udpServer;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed = false; private bool _disposed = false;
@@ -71,9 +73,8 @@ namespace Emby.Server.Implementations.EntryPoints
} }
_cancellationTokenSource.Cancel(); _cancellationTokenSource.Cancel();
_udpServer.Dispose();
_cancellationTokenSource.Dispose(); _cancellationTokenSource.Dispose();
_cancellationTokenSource = null; _udpServer?.Dispose();
_udpServer = null; _udpServer = null;
_disposed = true; _disposed = true;

View File

@@ -185,11 +185,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
updateToken = true; updateToken = true;
} }
authInfo.IsApiKey = true; authInfo.IsApiKey = false;
} }
else else
{ {
authInfo.IsApiKey = false; authInfo.IsApiKey = true;
} }
if (updateToken) if (updateToken)

View File

@@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext) public User GetUser(object requestContext)
{ {
return GetUser((HttpContext)requestContext); return GetUser(((HttpRequest)requestContext).HttpContext);
} }
} }
} }

View File

@@ -582,9 +582,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
{ {
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption));
} }
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
@@ -594,16 +592,16 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{ {
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and osx the search pattern is case sensitive // On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1) if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
{ {
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption)); return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
} }
var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption); var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
if (extensions != null && extensions.Count > 0) if (extensions != null && extensions.Count > 0)
{ {
@@ -625,10 +623,10 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{ {
var directoryInfo = new DirectoryInfo(path); var directoryInfo = new DirectoryInfo(path);
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var enumerationOptions = GetEnumerationOptions(recursive);
return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption)) return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption))); .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
} }
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos) private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@@ -638,8 +636,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false) public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
{ {
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
return Directory.EnumerateDirectories(path, "*", searchOption);
} }
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false) public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
@@ -649,16 +646,16 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false) public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{ {
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and osx the search pattern is case sensitive // On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1) if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
{ {
return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption); return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions);
} }
var files = Directory.EnumerateFiles(path, "*", searchOption); var files = Directory.EnumerateFiles(path, "*", enumerationOptions);
if (extensions != null && extensions.Length > 0) if (extensions != null && extensions.Length > 0)
{ {
@@ -679,8 +676,18 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false) public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{ {
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
return Directory.EnumerateFileSystemEntries(path, "*", searchOption); }
private EnumerationOptions GetEnumerationOptions(bool recursive)
{
return new EnumerationOptions
{
RecurseSubdirectories = recursive,
IgnoreInaccessible = true,
// Don't skip any files.
AttributesToSkip = 0
};
} }
private static void RunProcess(string path, string args, string workingDirectory) private static void RunProcess(string path, string args, string workingDirectory)

View File

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

View File

@@ -1,130 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library
{
/// <summary>
/// A library post scan/refresh task for pre-fetching remote images.
/// </summary>
public class ImageFetcherPostScanTask : ILibraryPostScanTask
{
private readonly ILibraryManager _libraryManager;
private readonly IProviderManager _providerManager;
private readonly ILogger<ImageFetcherPostScanTask> _logger;
private readonly SemaphoreSlim _imageFetcherLock;
private ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)> _queuedItems;
/// <summary>
/// Initializes a new instance of the <see cref="ImageFetcherPostScanTask"/> class.
/// </summary>
/// <param name="libraryManager">An instance of <see cref="ILibraryManager"/>.</param>
/// <param name="providerManager">An instance of <see cref="IProviderManager"/>.</param>
/// <param name="logger">An instance of <see cref="ILogger{ImageFetcherPostScanTask}"/>.</param>
public ImageFetcherPostScanTask(
ILibraryManager libraryManager,
IProviderManager providerManager,
ILogger<ImageFetcherPostScanTask> logger)
{
_libraryManager = libraryManager;
_providerManager = providerManager;
_logger = logger;
_queuedItems = new ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)>();
_imageFetcherLock = new SemaphoreSlim(1, 1);
_libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated;
_libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated;
_providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted;
}
/// <inheritdoc />
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
// Sometimes a library scan will cause this to run twice if there's an item refresh going on.
await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var now = DateTime.UtcNow;
var itemGuids = _queuedItems.Keys.ToList();
for (var i = 0; i < itemGuids.Count; i++)
{
if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem))
{
continue;
}
var itemId = queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture);
var itemType = queuedItem.item.GetType();
_logger.LogDebug(
"Updating remote images for item {ItemId} with media type {ItemMediaType}",
itemId,
itemType);
try
{
await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch images for {Type} item with id {ItemId}", itemType, itemId);
}
_queuedItems.TryRemove(queuedItem.item.Id, out _);
}
if (itemGuids.Count > 0)
{
_logger.LogInformation(
"Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.",
itemGuids.Count.ToString(CultureInfo.InvariantCulture),
(DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
}
else
{
_logger.LogDebug("No images were updated.");
}
}
finally
{
_imageFetcherLock.Release();
}
}
private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs)
{
if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0)
{
_queuedItems.AddOrUpdate(
itemChangeEventArgs.Item.Id,
(itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason),
(key, existingValue) => existingValue);
}
}
private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
{
if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0)
{
_queuedItems.AddOrUpdate(
e.Argument.Id,
(e.Argument, ItemUpdateType.None),
(key, existingValue) => existingValue);
}
// The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on
// the item that was refreshed regardless of children refreshes. So we take it as a signal
// that the refresh is entirely completed.
Run(null, CancellationToken.None).GetAwaiter().GetResult();
}
}
}

View File

@@ -42,7 +42,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library; using MediaBrowser.Model.Library;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo; using MediaBrowser.Providers.MediaInfo;
@@ -1241,11 +1240,20 @@ namespace Emby.Server.Implementations.Library
return info; return info;
} }
private string GetCollectionType(string path) private CollectionTypeOptions? GetCollectionType(string path)
{ {
return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false) var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
.Select(Path.GetFileNameWithoutExtension) foreach (var file in files)
.FirstOrDefault(i => !string.IsNullOrEmpty(i)); {
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
// https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
{
return res;
}
}
return null;
} }
/// <summary> /// <summary>
@@ -1955,9 +1963,12 @@ namespace Emby.Server.Implementations.Library
} }
/// <inheritdoc /> /// <inheritdoc />
public Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{ {
RunMetadataSavers(items, updateReason); foreach (var item in items)
{
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
}
_itemRepository.SaveItems(items, cancellationToken); _itemRepository.SaveItems(items, cancellationToken);
@@ -1988,25 +1999,22 @@ namespace Emby.Server.Implementations.Library
} }
} }
} }
return Task.CompletedTask;
} }
/// <inheritdoc /> /// <inheritdoc />
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
public void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason) public Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{ {
foreach (var item in items) if (item.IsFileProtocol)
{ {
if (item.IsFileProtocol) ProviderManager.SaveMetadata(item, updateReason);
{
ProviderManager.SaveMetadata(item, updateReason);
}
item.DateLastSaved = DateTime.UtcNow;
} }
item.DateLastSaved = DateTime.UtcNow;
return UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate);
} }
/// <summary> /// <summary>
@@ -2768,6 +2776,7 @@ namespace Emby.Server.Implementations.Library
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
{ {
string newPath;
if (ownerItem != null) if (ownerItem != null)
{ {
var libraryOptions = GetLibraryOptions(ownerItem); var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2775,15 +2784,9 @@ namespace Emby.Server.Implementations.Library
{ {
foreach (var pathInfo in libraryOptions.PathInfos) 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; return newPath;
}
var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
} }
} }
} }
@@ -2792,24 +2795,16 @@ namespace Emby.Server.Implementations.Library
var metadataPath = _configurationManager.Configuration.MetadataPath; var metadataPath = _configurationManager.Configuration.MetadataPath;
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath; 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); return newPath;
if (metadataSubstitutionResult.Item2)
{
return metadataSubstitutionResult.Item1;
}
} }
foreach (var map in _configurationManager.Configuration.PathSubstitutions) 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); return newPath;
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
}
} }
} }
@@ -2818,47 +2813,12 @@ namespace Emby.Server.Implementations.Library
public string SubstitutePath(string path, string from, string to) public string SubstitutePath(string path, string from, string to)
{ {
return SubstitutePathInternal(path, from, to).Item1; if (path.TryReplaceSubPath(from, to, out var newPath))
}
private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
{
if (string.IsNullOrWhiteSpace(path))
{ {
throw new ArgumentNullException(nameof(path)); return newPath;
} }
if (string.IsNullOrWhiteSpace(from)) return path;
{
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);
} }
private void SetExtraTypeFromFilename(Video item) private void SetExtraTypeFromFilename(Video item)
@@ -2957,7 +2917,7 @@ namespace Emby.Server.Implementations.Library
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary) public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
{ {
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name))
{ {
@@ -2991,9 +2951,9 @@ namespace Emby.Server.Implementations.Library
{ {
Directory.CreateDirectory(virtualFolderPath); Directory.CreateDirectory(virtualFolderPath);
if (!string.IsNullOrEmpty(collectionType)) if (collectionType != null)
{ {
var path = Path.Combine(virtualFolderPath, collectionType + ".collection"); var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty<byte>()); File.WriteAllBytes(path, Array.Empty<byte>());
} }

View File

@@ -1,6 +1,8 @@
#nullable enable #nullable enable
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
@@ -47,5 +49,59 @@ namespace Emby.Server.Implementations.Library
return null; 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

@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// Gets the priority. /// Gets the priority.
/// </summary> /// </summary>
/// <value>The priority.</value> /// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Fourth; public override ResolverPriority Priority => ResolverPriority.Fifth;
public MultiItemResolverResult ResolveMultiple( public MultiItemResolverResult ResolveMultiple(
Folder parent, Folder parent,

View File

@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// Gets the priority. /// Gets the priority.
/// </summary> /// </summary>
/// <value>The priority.</value> /// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Second; public override ResolverPriority Priority => ResolverPriority.Third;
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.

View File

@@ -79,11 +79,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return new MusicArtist(); return new MusicArtist();
} }
if (_config.Configuration.EnableSimpleArtistDetection)
{
return null;
}
// Avoid mis-identifying top folders // Avoid mis-identifying top folders
if (args.Parent.IsRoot) if (args.Parent.IsRoot)
{ {

View File

@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{ {
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book> public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
{ {
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" }; private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
protected override Book Resolve(ItemResolveArgs args) protected override Book Resolve(ItemResolveArgs args)
{ {

View File

@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// Gets the priority. /// Gets the priority.
/// </summary> /// </summary>
/// <value>The priority.</value> /// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Third; public override ResolverPriority Priority => ResolverPriority.Fourth;
/// <inheritdoc /> /// <inheritdoc />
public MultiItemResolverResult ResolveMultiple( public MultiItemResolverResult ResolveMultiple(

View File

@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
} }
// It's a directory-based playlist if the directory contains a playlist file // It's a directory-based playlist if the directory contains a playlist file
var filePaths = Directory.EnumerateFiles(args.Path); var filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase))) if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
{ {
return new Playlist return new Playlist

View File

@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library
if (query.Limit.HasValue) if (query.Limit.HasValue)
{ {
results = results.GetRange(0, query.Limit.Value); results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
} }
return new QueryResult<SearchHintInfo> return new QueryResult<SearchHintInfo>

View File

@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Book = MediaBrowser.Controller.Entities.Book; using Book = MediaBrowser.Controller.Entities.Book;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
{ {
@@ -219,7 +220,7 @@ namespace Emby.Server.Implementations.Library
var hasRuntime = runtimeTicks > 0; var hasRuntime = runtimeTicks > 0;
// If a position has been reported, and if we know the duration // If a position has been reported, and if we know the duration
if (positionTicks > 0 && hasRuntime) if (positionTicks > 0 && hasRuntime && !(item is AudioBook))
{ {
var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100; var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
@@ -245,6 +246,23 @@ 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;
if (minIn > _config.Configuration.MinAudiobookResume)
{
// ignore progress during the beginning
positionTicks = 0;
}
else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
{
// mark as completed close to the end
positionTicks = 0;
data.Played = playedToCompletion = true;
}
}
else if (!hasRuntime) else if (!hasRuntime)
{ {
// If we don't know the runtime we'll just have to assume it was fully played // If we don't know the runtime we'll just have to assume it was fully played

View File

@@ -129,23 +129,23 @@ namespace Emby.Server.Implementations.Library
if (!query.IncludeHidden) if (!query.IncludeHidden)
{ {
list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList(); list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
} }
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList(); var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
return list return list
.OrderBy(i => .OrderBy(i =>
{ {
var index = orders.IndexOf(i.Id.ToString("N", CultureInfo.InvariantCulture)); var index = Array.IndexOf(orders, i.Id);
if (index == -1 if (index == -1
&& i is UserView view && i is UserView view
&& view.DisplayParentId != Guid.Empty) && view.DisplayParentId != Guid.Empty)
{ {
index = orders.IndexOf(view.DisplayParentId.ToString("N", CultureInfo.InvariantCulture)); index = Array.IndexOf(orders, view.DisplayParentId);
} }
return index == -1 ? int.MaxValue : index; return index == -1 ? int.MaxValue : index;
@@ -280,8 +280,8 @@ namespace Emby.Server.Implementations.Library
{ {
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder) .Where(i => i is Folder)
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes) .Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) .Contains(i.Id))
.ToList(); .ToList();
} }

View File

@@ -45,7 +45,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); 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(); onStarted();
@@ -70,7 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); 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(); onStarted();

View File

@@ -1860,7 +1860,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; 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 var settings = new XmlWriterSettings
{ {
@@ -1924,7 +1925,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; 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 var settings = new XmlWriterSettings
{ {
@@ -2608,7 +2610,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
Locations = new string[] { customPath }, Locations = new string[] { customPath },
Name = "Recorded Movies", Name = "Recorded Movies",
CollectionType = CollectionType.Movies CollectionType = CollectionTypeOptions.Movies
}; };
} }
@@ -2619,7 +2621,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
Locations = new string[] { customPath }, Locations = new string[] { customPath },
Name = "Recorded Shows", Name = "Recorded Shows",
CollectionType = CollectionType.TvShows CollectionType = CollectionTypeOptions.TvShows
}; };
} }
} }

View File

@@ -93,7 +93,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length); _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);

View File

@@ -611,25 +611,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken, CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{ {
try var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{ {
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false); return response;
}
catch (HttpRequestException ex)
{
_tokens.Clear();
if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
{
enableRetry = false;
}
if (!enableRetry)
{
throw;
}
} }
// Response is automatically disposed in the calling function,
// so dispose manually if not returning.
response.Dispose();
if (!enableRetry || (int)response.StatusCode >= 500)
{
throw new HttpRequestException(
string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase),
null,
response.StatusCode);
}
_tokens.Clear();
options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false)); options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false); return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
} }
@@ -647,6 +647,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json); options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false); var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
if (string.Equals(root.message, "OK", StringComparison.Ordinal)) if (string.Equals(root.message, "OK", StringComparison.Ordinal))
@@ -701,6 +702,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try try
{ {
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content; using var response = httpResponse.Content;
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false); var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
@@ -709,7 +711,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)
{ {
// Apparently we're supposed to swallow this // SchedulesDirect returns 400 if no lineups are configured.
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest) if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
{ {
return false; return false;
@@ -782,18 +784,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var allStations = root.stations ?? new List<ScheduleDirect.Station>(); var allStations = root.stations ?? new List<ScheduleDirect.Station>();
var map = root.map; var map = root.map;
int len = map.Count; var list = new List<ChannelInfo>(map.Count);
var array = new List<ChannelInfo>(len); foreach (var channel in map)
for (int i = 0; i < len; i++)
{ {
var channelNumber = GetChannelNumber(map[i]); var channelNumber = GetChannelNumber(channel);
var station = allStations.Find(item => string.Equals(item.stationID, map[i].stationID, StringComparison.OrdinalIgnoreCase)); var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase));
if (station == null) if (station == null)
{ {
station = new ScheduleDirect.Station station = new ScheduleDirect.Station
{ {
stationID = map[i].stationID stationID = channel.stationID
}; };
} }
@@ -810,10 +811,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
channelInfo.ImageUrl = station.logo.URL; channelInfo.ImageUrl = station.logo.URL;
} }
array[i] = channelInfo; list.Add(channelInfo);
} }
return array; return list;
} }
private static string NormalizeName(string value) private static string NormalizeName(string value)

View File

@@ -1928,7 +1928,7 @@ namespace Emby.Server.Implementations.LiveTv
foreach (var programDto in currentProgramDtos) foreach (var programDto in currentProgramDtos)
{ {
if (currentChannelsDict.TryGetValue(programDto.ChannelId, out BaseItemDto channelDto)) if (programDto.ChannelId.HasValue && currentChannelsDict.TryGetValue(programDto.ChannelId.Value, out BaseItemDto channelDto))
{ {
channelDto.CurrentProgram = programDto; channelDto.CurrentProgram = programDto;
} }
@@ -2018,7 +2018,7 @@ namespace Emby.Server.Implementations.LiveTv
info.DayPattern = _tvDtoService.GetDayPattern(info.Days); info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
info.Name = program.Name; info.Name = program.Name;
info.ChannelId = programDto.ChannelId; info.ChannelId = programDto.ChannelId ?? Guid.Empty;
info.ChannelName = programDto.ChannelName; info.ChannelName = programDto.ChannelName;
info.StartDate = program.StartDate; info.StartDate = program.StartDate;
info.Name = program.Name; info.Name = program.Name;

View File

@@ -0,0 +1,21 @@
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
internal class Channels
{
public string GuideNumber { get; set; }
public string GuideName { get; set; }
public string VideoCodec { get; set; }
public string AudioCodec { get; set; }
public string URL { get; set; }
public bool Favorite { get; set; }
public bool DRM { get; set; }
public bool HD { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using System;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
internal class DiscoverResponse
{
public string FriendlyName { get; set; }
public string ModelNumber { get; set; }
public string FirmwareName { get; set; }
public string FirmwareVersion { get; set; }
public string DeviceID { get; set; }
public string DeviceAuth { get; set; }
public string BaseURL { get; set; }
public string LineupURL { get; set; }
public int TunerCount { get; set; }
public bool SupportsTranscoding
{
get
{
var model = ModelNumber ?? string.Empty;
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
return false;
}
}
}
}

View File

@@ -8,10 +8,12 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@@ -37,6 +39,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper; private readonly IStreamHelper _streamHelper;
private readonly JsonSerializerOptions _jsonOptions;
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>(); private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
public HdHomerunHost( public HdHomerunHost(
@@ -56,6 +60,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_socketFactory = socketFactory; _socketFactory = socketFactory;
_networkManager = networkManager; _networkManager = networkManager;
_streamHelper = streamHelper; _streamHelper = streamHelper;
_jsonOptions = JsonDefaults.GetOptions();
} }
public string Name => "HD Homerun"; public string Name => "HD Homerun";
@@ -67,13 +73,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private string GetChannelId(TunerHostInfo info, Channels i) private string GetChannelId(TunerHostInfo info, Channels i)
=> ChannelIdPrefix + i.GuideNumber; => ChannelIdPrefix + i.GuideNumber;
private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken) internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
{ {
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken) var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>(); .ConfigureAwait(false) ?? new List<Channels>();
if (info.ImportFavoritesOnly) if (info.ImportFavoritesOnly)
@@ -100,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Id = GetChannelId(info, i), Id = GetChannelId(info, i),
IsFavorite = i.Favorite, IsFavorite = i.Favorite,
TunerHostId = info.Id, TunerHostId = info.Id,
IsHD = i.HD == 1, IsHD = i.HD,
AudioCodec = i.AudioCodec, AudioCodec = i.AudioCodec,
VideoCodec = i.VideoCodec, VideoCodec = i.VideoCodec,
ChannelType = ChannelType.TV, ChannelType = ChannelType.TV,
@@ -109,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}).Cast<ChannelInfo>().ToList(); }).Cast<ChannelInfo>().ToList();
} }
private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken) internal async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
{ {
var cacheKey = info.Id; var cacheKey = info.Id;
@@ -127,10 +133,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try try
{ {
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken) var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey)) if (!string.IsNullOrEmpty(cacheKey))
@@ -328,25 +335,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new Uri(url).AbsoluteUri.TrimEnd('/'); return new Uri(url).AbsoluteUri.TrimEnd('/');
} }
private class Channels
{
public string GuideNumber { get; set; }
public string GuideName { get; set; }
public string VideoCodec { get; set; }
public string AudioCodec { get; set; }
public string URL { get; set; }
public bool Favorite { get; set; }
public bool DRM { get; set; }
public int HD { get; set; }
}
protected EncodingOptions GetEncodingOptions() protected EncodingOptions GetEncodingOptions()
{ {
return Config.GetConfiguration<EncodingOptions>("encoding"); return Config.GetConfiguration<EncodingOptions>("encoding");
@@ -674,42 +662,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
public class DiscoverResponse
{
public string FriendlyName { get; set; }
public string ModelNumber { get; set; }
public string FirmwareName { get; set; }
public string FirmwareVersion { get; set; }
public string DeviceID { get; set; }
public string DeviceAuth { get; set; }
public string BaseURL { get; set; }
public string LineupURL { get; set; }
public int TunerCount { get; set; }
public bool SupportsTranscoding
{
get
{
var model = ModelNumber ?? string.Empty;
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
return false;
}
}
}
public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken) public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
{ {
lock (_modelCache) lock (_modelCache)
@@ -762,7 +714,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return list; return list;
} }
private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken) internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
{ {
var hostInfo = new TunerHostInfo var hostInfo = new TunerHostInfo
{ {
@@ -774,6 +726,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
hostInfo.DeviceId = modelInfo.DeviceID; hostInfo.DeviceId = modelInfo.DeviceID;
hostInfo.FriendlyName = modelInfo.FriendlyName; hostInfo.FriendlyName = modelInfo.FriendlyName;
hostInfo.TunerCount = modelInfo.TunerCount;
return hostInfo; return hostInfo;
} }

View File

@@ -193,7 +193,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
var resolved = false; var resolved = false;
using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
{ {
while (true) while (true)
{ {

View File

@@ -136,7 +136,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath); Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
using var message = response; using var message = response;
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
await StreamHelper.CopyToAsync( await StreamHelper.CopyToAsync(
stream, stream,
fileStream, fileStream,

View File

@@ -77,6 +77,8 @@ chb|||Chibcha|chibcha
che||ce|Chechen|tchétchène che||ce|Chechen|tchétchène
chg|||Chagatai|djaghataï chg|||Chagatai|djaghataï
chi|zho|zh|Chinese|chinois chi|zho|zh|Chinese|chinois
chi|zho|zh-tw|Chinese; Traditional|chinois
chi|zho|zh-hk|Chinese; Hong Kong|chinois
chk|||Chuukese|chuuk chk|||Chuukese|chuuk
chm|||Mari|mari chm|||Mari|mari
chn|||Chinook jargon|chinook, jargon chn|||Chinook jargon|chinook, jargon

View File

@@ -0,0 +1,763 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Plugins
{
/// <summary>
/// Defines the <see cref="PluginManager" />.
/// </summary>
public class PluginManager : IPluginManager
{
private readonly string _pluginsPath;
private readonly Version _appVersion;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger<PluginManager> _logger;
private readonly IApplicationHost _appHost;
private readonly ServerConfiguration _config;
private readonly IList<LocalPlugin> _plugins;
private readonly Version _minimumVersion;
private IHttpClientFactory? _httpClientFactory;
private IHttpClientFactory HttpClientFactory
{
get
{
if (_httpClientFactory == null)
{
_httpClientFactory = _appHost.Resolve<IHttpClientFactory>();
}
return _httpClientFactory;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
/// <param name="config">The <see cref="ServerConfiguration"/>.</param>
/// <param name="pluginsPath">The plugin path.</param>
/// <param name="appVersion">The application version.</param>
public PluginManager(
ILogger<PluginManager> logger,
IApplicationHost appHost,
ServerConfiguration config,
string pluginsPath,
Version appVersion)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_pluginsPath = pluginsPath;
_appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
_jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions())
{
WriteIndented = true
};
// We need to use the default GUID converter, so we need to remove any custom ones.
for (int a = _jsonOptions.Converters.Count - 1; a >= 0; a--)
{
if (_jsonOptions.Converters[a] is JsonGuidConverter convertor)
{
_jsonOptions.Converters.Remove(convertor);
break;
}
}
_config = config;
_appHost = appHost;
_minimumVersion = new Version(0, 0, 0, 1);
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
}
/// <summary>
/// Gets the Plugins.
/// </summary>
public IList<LocalPlugin> Plugins => _plugins;
/// <summary>
/// Returns all the assemblies.
/// </summary>
/// <returns>An IEnumerable{Assembly}.</returns>
public IEnumerable<Assembly> LoadAssemblies()
{
// Attempt to remove any deleted plugins and change any successors to be active.
for (int i = _plugins.Count - 1; i >= 0; i--)
{
var plugin = _plugins[i];
if (plugin.Manifest.Status == PluginStatus.Deleted && DeletePlugin(plugin))
{
// See if there is another version, and if so make that active.
ProcessAlternative(plugin);
}
}
// Now load the assemblies..
foreach (var plugin in _plugins)
{
UpdatePluginSuperceedStatus(plugin);
if (plugin.IsEnabledAndSupported == false)
{
_logger.LogInformation("Skipping disabled plugin {Version} of {Name} ", plugin.Version, plugin.Name);
continue;
}
foreach (var file in plugin.DllFiles)
{
Assembly assembly;
try
{
assembly = Assembly.LoadFrom(file);
assembly.GetExportedTypes();
}
catch (FileLoadException ex)
{
_logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
ChangePluginState(plugin, PluginStatus.Malfunctioned);
continue;
}
catch (TypeLoadException ex) // Undocumented exception
{
_logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
ChangePluginState(plugin, PluginStatus.NotSupported);
continue;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file);
ChangePluginState(plugin, PluginStatus.Malfunctioned);
continue;
}
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
yield return assembly;
}
}
}
/// <summary>
/// Creates all the plugin instances.
/// </summary>
public void CreatePlugins()
{
_ = _appHost.GetExports<IPlugin>(CreatePluginInstance)
.Where(i => i != null)
.ToArray();
}
/// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
/// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param>
public void RegisterServices(IServiceCollection serviceCollection)
{
foreach (var pluginServiceRegistrator in _appHost.GetExportTypes<IPluginServiceRegistrator>())
{
var plugin = GetPluginByAssembly(pluginServiceRegistrator.Assembly);
if (plugin == null)
{
_logger.LogError("Unable to find plugin in assembly {Assembly}", pluginServiceRegistrator.Assembly.FullName);
continue;
}
UpdatePluginSuperceedStatus(plugin);
if (!plugin.IsEnabledAndSupported)
{
continue;
}
try
{
var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator);
instance?.RegisterServices(serviceCollection);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly.FullName);
if (ChangePluginState(plugin, PluginStatus.Malfunctioned))
{
_logger.LogInformation("Disabling plugin {Path}", plugin.Path);
}
}
}
}
/// <summary>
/// Imports a plugin manifest from <paramref name="folder"/>.
/// </summary>
/// <param name="folder">Folder of the plugin.</param>
public void ImportPluginFrom(string folder)
{
if (string.IsNullOrEmpty(folder))
{
throw new ArgumentNullException(nameof(folder));
}
// Load the plugin.
var plugin = LoadManifest(folder);
// Make sure we haven't already loaded this.
if (_plugins.Any(p => p.Manifest.Equals(plugin.Manifest)))
{
return;
}
_plugins.Add(plugin);
EnablePlugin(plugin);
}
/// <summary>
/// Removes the plugin reference '<paramref name="plugin"/>.
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <returns>Outcome of the operation.</returns>
public bool RemovePlugin(LocalPlugin plugin)
{
if (plugin == null)
{
throw new ArgumentNullException(nameof(plugin));
}
if (DeletePlugin(plugin))
{
ProcessAlternative(plugin);
return true;
}
_logger.LogWarning("Unable to delete {Path}, so marking as deleteOnStartup.", plugin.Path);
// Unable to delete, so disable.
if (ChangePluginState(plugin, PluginStatus.Deleted))
{
ProcessAlternative(plugin);
return true;
}
return false;
}
/// <summary>
/// Attempts to find the plugin with and id of <paramref name="id"/>.
/// </summary>
/// <param name="id">The <see cref="Guid"/> of plugin.</param>
/// <param name="version">Optional <see cref="Version"/> of the plugin to locate.</param>
/// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns>
public LocalPlugin? GetPlugin(Guid id, Version? version = null)
{
LocalPlugin? plugin;
if (version == null)
{
// If no version is given, return the current instance.
var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList();
plugin = plugins.FirstOrDefault(p => p.Instance != null);
if (plugin == null)
{
plugin = plugins.OrderByDescending(p => p.Version).FirstOrDefault();
}
}
else
{
// Match id and version number.
plugin = _plugins.FirstOrDefault(p => p.Id.Equals(id) && p.Version.Equals(version));
}
return plugin;
}
/// <summary>
/// Enables the plugin, disabling all other versions.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
public void EnablePlugin(LocalPlugin plugin)
{
if (plugin == null)
{
throw new ArgumentNullException(nameof(plugin));
}
if (ChangePluginState(plugin, PluginStatus.Active))
{
// See if there is another version, and if so, supercede it.
ProcessAlternative(plugin);
}
}
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
public void DisablePlugin(LocalPlugin plugin)
{
if (plugin == null)
{
throw new ArgumentNullException(nameof(plugin));
}
// Update the manifest on disk
if (ChangePluginState(plugin, PluginStatus.Disabled))
{
// If there is another version, activate it.
ProcessAlternative(plugin);
}
}
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param>
public void FailPlugin(Assembly assembly)
{
// Only save if disabled.
if (assembly == null)
{
throw new ArgumentNullException(nameof(assembly));
}
var plugin = _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location));
if (plugin == null)
{
// A plugin's assembly didn't cause this issue, so ignore it.
return;
}
ChangePluginState(plugin, PluginStatus.Malfunctioned);
}
/// <inheritdoc/>
public bool SaveManifest(PluginManifest manifest, string path)
{
try
{
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
File.WriteAllText(Path.Combine(path, "meta.json"), data);
return true;
}
catch (ArgumentException e)
{
_logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path);
return false;
}
}
/// <inheritdoc/>
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path)
{
if (packageInfo == null)
{
return false;
}
var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
var imagePath = string.Empty;
if (!string.IsNullOrEmpty(packageInfo.ImageUrl))
{
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
await using var fileStream = File.OpenWrite(imagePath);
try
{
await using var downloadStream = await HttpClientFactory
.CreateClient(NamedClient.Default)
.GetStreamAsync(url)
.ConfigureAwait(false);
await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
imagePath = string.Empty;
}
}
var manifest = new PluginManifest
{
Category = packageInfo.Category,
Changelog = versionInfo.Changelog ?? string.Empty,
Description = packageInfo.Description,
Id = new Guid(packageInfo.Id),
Name = packageInfo.Name,
Overview = packageInfo.Overview,
Owner = packageInfo.Owner,
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp),
Version = versionInfo.Version,
Status = PluginStatus.Active,
AutoUpdate = true,
ImagePath = imagePath
};
return SaveManifest(manifest, path);
}
/// <summary>
/// Changes a plugin's load status.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> instance.</param>
/// <param name="state">The <see cref="PluginStatus"/> of the plugin.</param>
/// <returns>Success of the task.</returns>
private bool ChangePluginState(LocalPlugin plugin, PluginStatus state)
{
if (plugin.Manifest.Status == state || string.IsNullOrEmpty(plugin.Path))
{
// No need to save as the state hasn't changed.
return true;
}
plugin.Manifest.Status = state;
return SaveManifest(plugin.Manifest, plugin.Path);
}
/// <summary>
/// Finds the plugin record using the assembly.
/// </summary>
/// <param name="assembly">The <see cref="Assembly"/> being sought.</param>
/// <returns>The matching record, or null if not found.</returns>
private LocalPlugin? GetPluginByAssembly(Assembly assembly)
{
// Find which plugin it is by the path.
return _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location, StringComparer.Ordinal));
}
/// <summary>
/// Creates the instance safe.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>System.Object.</returns>
private IPlugin? CreatePluginInstance(Type type)
{
// Find the record for this plugin.
var plugin = GetPluginByAssembly(type.Assembly);
if (plugin?.Manifest.Status < PluginStatus.Active)
{
return null;
}
try
{
_logger.LogDebug("Creating instance of {Type}", type);
var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
if (plugin == null)
{
// Create a dummy record for the providers.
// TODO: remove this code once all provided have been released as separate plugins.
plugin = new LocalPlugin(
instance.AssemblyFilePath,
true,
new PluginManifest
{
Id = instance.Id,
Status = PluginStatus.Active,
Name = instance.Name,
Version = instance.Version.ToString()
})
{
Instance = instance
};
_plugins.Add(plugin);
plugin.Manifest.Status = PluginStatus.Active;
}
else
{
plugin.Instance = instance;
var manifest = plugin.Manifest;
var pluginStr = instance.Version.ToString();
bool changed = false;
if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal)
|| manifest.Id != instance.Id)
{
// If a plugin without a manifest failed to load due to an external issue (eg config),
// this updates the manifest to the actual plugin values.
manifest.Version = pluginStr;
manifest.Name = plugin.Instance.Name;
manifest.Description = plugin.Instance.Description;
manifest.Id = plugin.Instance.Id;
changed = true;
}
changed = changed || manifest.Status != PluginStatus.Active;
manifest.Status = PluginStatus.Active;
if (changed)
{
SaveManifest(manifest, plugin.Path);
}
}
_logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
return instance;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Error creating {Type}", type.FullName);
if (plugin != null)
{
if (ChangePluginState(plugin, PluginStatus.Malfunctioned))
{
_logger.LogInformation("Plugin {Path} has been disabled.", plugin.Path);
return null;
}
}
_logger.LogDebug("Unable to auto-disable.");
return null;
}
}
private void UpdatePluginSuperceedStatus(LocalPlugin plugin)
{
if (plugin.Manifest.Status != PluginStatus.Superceded)
{
return;
}
var predecessor = _plugins.OrderByDescending(p => p.Version)
.FirstOrDefault(p => p.Id.Equals(plugin.Id) && p.IsEnabledAndSupported && p.Version != plugin.Version);
if (predecessor != null)
{
return;
}
plugin.Manifest.Status = PluginStatus.Active;
}
/// <summary>
/// Attempts to delete a plugin.
/// </summary>
/// <param name="plugin">A <see cref="LocalPlugin"/> instance to delete.</param>
/// <returns>True if successful.</returns>
private bool DeletePlugin(LocalPlugin plugin)
{
// Attempt a cleanup of old folders.
try
{
Directory.Delete(plugin.Path, true);
_logger.LogDebug("Deleted {Path}", plugin.Path);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
return false;
}
return _plugins.Remove(plugin);
}
private LocalPlugin LoadManifest(string dir)
{
Version? version;
PluginManifest? manifest = null;
var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
try
{
var data = File.ReadAllText(metafile, Encoding.UTF8);
manifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Error deserializing {Path}.", dir);
}
}
if (manifest != null)
{
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
targetAbi = _minimumVersion;
}
if (!Version.TryParse(manifest.Version, out version))
{
manifest.Version = _minimumVersion.ToString();
}
return new LocalPlugin(dir, _appVersion >= targetAbi, manifest);
}
// No metafile, so lets see if the folder is versioned.
// TODO: Phase this support out in future versions.
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1)
{
// Get the version number from the filename if possible.
metafile = Path.GetFileName(dir[..versionIndex]) ?? dir[..versionIndex];
version = Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version? parsedVersion) ? parsedVersion : _appVersion;
}
else
{
// Un-versioned folder - Add it under the path name and version it suitable for this instance.
version = _appVersion;
}
// Auto-create a plugin manifest, so we can disable it, if it fails to load.
manifest = new PluginManifest
{
Status = PluginStatus.Active,
Name = metafile,
AutoUpdate = false,
Id = metafile.GetMD5(),
TargetAbi = _appVersion.ToString(),
Version = version.ToString()
};
return new LocalPlugin(dir, true, manifest);
}
/// <summary>
/// Gets the list of local plugins.
/// </summary>
/// <returns>Enumerable of local plugins.</returns>
private IEnumerable<LocalPlugin> DiscoverPlugins()
{
var versions = new List<LocalPlugin>();
if (!Directory.Exists(_pluginsPath))
{
// Plugin path doesn't exist, don't try to enumerate sub-folders.
return Enumerable.Empty<LocalPlugin>();
}
var directories = Directory.EnumerateDirectories(_pluginsPath, "*.*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
versions.Add(LoadManifest(dir));
}
string lastName = string.Empty;
versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{
entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
if (entry.IsEnabledAndSupported)
{
lastName = entry.Name;
continue;
}
}
if (string.IsNullOrEmpty(lastName))
{
continue;
}
var manifest = entry.Manifest;
var cleaned = false;
var path = entry.Path;
if (_config.RemoveOldPlugins)
{
// Attempt a cleanup of old folders.
try
{
_logger.LogDebug("Deleting {Path}", path);
Directory.Delete(path, true);
cleaned = true;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception e)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogWarning(e, "Unable to delete {Path}", path);
}
if (cleaned)
{
versions.RemoveAt(x);
}
else
{
if (manifest == null)
{
_logger.LogWarning("Unable to disable plugin {Path}", entry.Path);
continue;
}
ChangePluginState(entry, PluginStatus.Deleted);
}
}
}
// Only want plugin folders which have files.
return versions.Where(p => p.DllFiles.Count != 0);
}
/// <summary>
/// Changes the status of the other versions of the plugin to "Superceded".
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> that's master.</param>
private void ProcessAlternative(LocalPlugin plugin)
{
// Detect whether there is another version of this plugin that needs disabling.
var previousVersion = _plugins.OrderByDescending(p => p.Version)
.FirstOrDefault(
p => p.Id.Equals(plugin.Id)
&& p.IsEnabledAndSupported
&& p.Version != plugin.Version);
if (previousVersion == null)
{
// This value is memory only - so that the web will show restart required.
plugin.Manifest.Status = PluginStatus.Restart;
return;
}
if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superceded))
{
_logger.LogError("Unable to enable version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
else if (plugin.Manifest.Status == PluginStatus.Superceded && !ChangePluginState(previousVersion, PluginStatus.Active))
{
_logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
// This value is memory only - so that the web will show restart required.
plugin.Manifest.Status = PluginStatus.Restart;
}
}
}

View File

@@ -1,60 +0,0 @@
using System;
namespace Emby.Server.Implementations.Plugins
{
/// <summary>
/// Defines a Plugin manifest file.
/// </summary>
public class PluginManifest
{
/// <summary>
/// Gets or sets the category of the plugin.
/// </summary>
public string Category { get; set; }
/// <summary>
/// Gets or sets the changelog information.
/// </summary>
public string Changelog { get; set; }
/// <summary>
/// Gets or sets the description of the plugin.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets the Global Unique Identifier for the plugin.
/// </summary>
public Guid Guid { get; set; }
/// <summary>
/// Gets or sets the Name of the plugin.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets an overview of the plugin.
/// </summary>
public string Overview { get; set; }
/// <summary>
/// Gets or sets the owner of the plugin.
/// </summary>
public string Owner { get; set; }
/// <summary>
/// Gets or sets the compatibility version for the plugin.
/// </summary>
public string TargetAbi { get; set; }
/// <summary>
/// Gets or sets the timestamp of the plugin.
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// Gets or sets the Version number of the plugin.
/// </summary>
public string Version { get; set; }
}
}

View File

@@ -1,5 +1,6 @@
using System.Reflection; using System.Reflection;
using System.Resources; using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following // General Information about an assembly is controlled through the following
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")] [assembly: NeutralResourcesLanguage("en")]
[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible // Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from // to COM components. If you need to access a type in this assembly from

View File

@@ -80,10 +80,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
// Delete log files more than n days old // Delete log files more than n days old
var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays); var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
// Only delete the .txt log files, the *.log files created by serilog get managed by itself // Only delete files that serilog doesn't manage (anything that doesn't start with 'log_'
var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true) var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true)
.Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) .Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal)
.ToList(); && _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
.ToList();
var index = 0; var index = 0;

View File

@@ -8,10 +8,10 @@ using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks namespace Emby.Server.Implementations.ScheduledTasks
{ {

View File

@@ -128,6 +128,9 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc /> /// <inheritdoc />
public event EventHandler<SessionEventArgs> SessionActivity; public event EventHandler<SessionEventArgs> SessionActivity;
/// <inheritdoc />
public event EventHandler<SessionEventArgs> SessionControllerConnected;
/// <summary> /// <summary>
/// Gets all connections. /// Gets all connections.
/// </summary> /// </summary>
@@ -312,6 +315,19 @@ namespace Emby.Server.Implementations.Session
return session; return session;
} }
/// <inheritdoc />
public void OnSessionControllerConnected(SessionInfo info)
{
EventHelper.QueueEventIfNotNull(
SessionControllerConnected,
this,
new SessionEventArgs
{
SessionInfo = info
},
_logger);
}
/// <inheritdoc /> /// <inheritdoc />
public void CloseIfNeeded(SessionInfo session) public void CloseIfNeeded(SessionInfo session)
{ {
@@ -1294,7 +1310,7 @@ namespace Emby.Server.Implementations.Session
} }
} }
return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken); return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
} }
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession) private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
@@ -1440,7 +1456,12 @@ namespace Emby.Server.Implementations.Session
throw new SecurityException("Unknown quick connect token"); throw new SecurityException("Unknown quick connect token");
} }
request.UserId = result.Items[0].UserId; var info = result.Items[0];
request.UserId = info.UserId;
// There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token.
_authRepo.Delete(info);
return AuthenticateNewSessionInternal(request, false); return AuthenticateNewSessionInternal(request, false);
} }

View File

@@ -133,6 +133,8 @@ namespace Emby.Server.Implementations.Session
var controller = (WebSocketController)controllerInfo.Item1; var controller = (WebSocketController)controllerInfo.Item1;
controller.AddWebSocket(connection); controller.AddWebSocket(connection);
_sessionManager.OnSessionControllerConnected(session);
} }
/// <summary> /// <summary>

View File

@@ -41,6 +41,12 @@ namespace Emby.Server.Implementations.SyncPlay
/// </summary> /// </summary>
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
/// <summary>
/// The map between users and counter of active sessions.
/// </summary>
private readonly ConcurrentDictionary<Guid, int> _activeUsers =
new ConcurrentDictionary<Guid, int>();
/// <summary> /// <summary>
/// The map between sessions and groups. /// The map between sessions and groups.
/// </summary> /// </summary>
@@ -81,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
_sessionManager = sessionManager; _sessionManager = sessionManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncPlayManager>(); _logger = loggerFactory.CreateLogger<SyncPlayManager>();
_sessionManager.SessionStarted += OnSessionManagerSessionStarted; _sessionManager.SessionControllerConnected += OnSessionControllerConnected;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!"); throw new InvalidOperationException("Could not add session to group!");
} }
UpdateSessionsCounter(session.UserId, 1);
group.CreateGroup(session, request, cancellationToken); group.CreateGroup(session, request, cancellationToken);
} }
} }
@@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay
if (existingGroup.GroupId.Equals(request.GroupId)) if (existingGroup.GroupId.Equals(request.GroupId))
{ {
// Restore session. // Restore session.
UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken); group.SessionJoin(session, request, cancellationToken);
return; return;
} }
@@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!"); throw new InvalidOperationException("Could not add session to group!");
} }
UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken); group.SessionJoin(session, request, cancellationToken);
} }
} }
@@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not remove session from group!"); throw new InvalidOperationException("Could not remove session from group!");
} }
UpdateSessionsCounter(session.UserId, -1);
group.SessionLeave(session, request, cancellationToken); group.SessionLeave(session, request, cancellationToken);
if (group.IsGroupEmpty()) if (group.IsGroupEmpty())
@@ -318,6 +328,19 @@ namespace Emby.Server.Implementations.SyncPlay
} }
} }
/// <inheritdoc />
public bool IsUserActive(Guid userId)
{
if (_activeUsers.TryGetValue(userId, out var sessionsCounter))
{
return sessionsCounter > 0;
}
else
{
return false;
}
}
/// <summary> /// <summary>
/// Releases unmanaged and optionally managed resources. /// Releases unmanaged and optionally managed resources.
/// </summary> /// </summary>
@@ -329,11 +352,11 @@ namespace Emby.Server.Implementations.SyncPlay
return; return;
} }
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted; _sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
_disposed = true; _disposed = true;
} }
private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e) private void OnSessionControllerConnected(object sender, SessionEventArgs e)
{ {
var session = e.SessionInfo; var session = e.SessionInfo;
@@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay
JoinGroup(session, request, CancellationToken.None); JoinGroup(session, request, CancellationToken.None);
} }
} }
private void UpdateSessionsCounter(Guid userId, int toAdd)
{
// Update sessions counter.
var newSessionsCounter = _activeUsers.AddOrUpdate(
userId,
1,
(key, sessionsCounter) => sessionsCounter + toAdd);
// Should never happen.
if (newSessionsCounter < 0)
{
throw new InvalidOperationException("Sessions counter is negative!");
}
// Clean record if user has no more active sessions.
if (newSessionsCounter == 0)
{
_activeUsers.TryRemove(new KeyValuePair<Guid, int>(userId, newSessionsCounter));
}
}
} }
} }

View File

@@ -75,8 +75,7 @@ namespace Emby.Server.Implementations.TV
{ {
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder) .Where(i => i is Folder)
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes) .Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes).Contains(i.Id))
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.ToArray(); .ToArray();
} }
@@ -144,10 +143,31 @@ namespace Emby.Server.Implementations.TV
var allNextUp = seriesKeys var allNextUp = seriesKeys
.Select(i => GetNextUp(i, currentUser, dtoOptions)); .Select(i => GetNextUp(i, currentUser, dtoOptions));
// If viewing all next up for all series, remove first episodes
// But if that returns empty, keep those first episodes (avoid completely empty view)
var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId);
var anyFound = false;
return allNextUp return allNextUp
.Where(i => .Where(i =>
{ {
return i.Item1 != DateTime.MinValue; if (request.DisableFirstEpisode)
{
return i.Item1 != DateTime.MinValue;
}
if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue)
{
anyFound = true;
return true;
}
if (!anyFound && i.Item1 == DateTime.MinValue)
{
return true;
}
return false;
}) })
.Select(i => i.Item2()) .Select(i => i.Item2())
.Where(i => i != null); .Where(i => i != null);

View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591 #nullable enable
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@@ -12,7 +12,6 @@ using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json; using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@@ -41,17 +40,15 @@ namespace Emby.Server.Implementations.Updates
private readonly IEventManager _eventManager; private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonSerializerOptions; private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly IPluginManager _pluginManager;
/// <summary> /// <summary>
/// Gets the application host. /// Gets the application host.
/// </summary> /// </summary>
/// <value>The application host.</value> /// <value>The application host.</value>
private readonly IServerApplicationHost _applicationHost; private readonly IServerApplicationHost _applicationHost;
private readonly IZipClient _zipClient; private readonly IZipClient _zipClient;
private readonly object _currentInstallationsLock = new object(); private readonly object _currentInstallationsLock = new object();
/// <summary> /// <summary>
@@ -64,6 +61,17 @@ namespace Emby.Server.Implementations.Updates
/// </summary> /// </summary>
private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal; private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
/// <summary>
/// Initializes a new instance of the <see cref="InstallationManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{InstallationManager}"/>.</param>
/// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
/// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
/// <param name="eventManager">The <see cref="IEventManager"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
/// <param name="zipClient">The <see cref="IZipClient"/>.</param>
/// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
public InstallationManager( public InstallationManager(
ILogger<InstallationManager> logger, ILogger<InstallationManager> logger,
IServerApplicationHost appHost, IServerApplicationHost appHost,
@@ -71,8 +79,8 @@ namespace Emby.Server.Implementations.Updates
IEventManager eventManager, IEventManager eventManager,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IServerConfigurationManager config, IServerConfigurationManager config,
IFileSystem fileSystem, IZipClient zipClient,
IZipClient zipClient) IPluginManager pluginManager)
{ {
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>(); _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>(); _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
@@ -83,38 +91,65 @@ namespace Emby.Server.Implementations.Updates
_eventManager = eventManager; _eventManager = eventManager;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_config = config; _config = config;
_fileSystem = fileSystem;
_zipClient = zipClient; _zipClient = zipClient;
_jsonSerializerOptions = JsonDefaults.GetOptions(); _jsonSerializerOptions = JsonDefaults.GetOptions();
_pluginManager = pluginManager;
} }
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc /> /// <inheritdoc />
public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default) public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{ {
try try
{ {
var packages = await _httpClientFactory.CreateClient(NamedClient.Default) List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
if (packages == null) if (packages == null)
{ {
return Array.Empty<PackageInfo>(); return Array.Empty<PackageInfo>();
} }
var minimumVersion = new Version(0, 0, 0, 1);
// Store the repository and repository url with each version, as they may be spread apart. // Store the repository and repository url with each version, as they may be spread apart.
foreach (var entry in packages) foreach (var entry in packages)
{ {
foreach (var ver in entry.versions) for (int a = entry.Versions.Count - 1; a >= 0; a--)
{ {
ver.repositoryName = manifestName; var ver = entry.Versions[a];
ver.repositoryUrl = manifest; ver.RepositoryName = manifestName;
ver.RepositoryUrl = manifest;
if (!filterIncompatible)
{
continue;
}
if (!Version.TryParse(ver.TargetAbi, out var targetAbi))
{
targetAbi = minimumVersion;
}
// Only show plugins that are greater than or equal to targetAbi.
if (_applicationHost.ApplicationVersion >= targetAbi)
{
continue;
}
// Not compatible with this version so remove it.
entry.Versions.Remove(ver);
} }
} }
return packages; return packages;
} }
catch (IOException ex)
{
_logger.LogError(ex, "Cannot locate the plugin manifest {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
catch (JsonException ex) catch (JsonException ex)
{ {
_logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest); _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
@@ -132,69 +167,53 @@ namespace Emby.Server.Implementations.Updates
} }
} }
private static void MergeSort(IList<VersionInfo> source, IList<VersionInfo> dest)
{
int sLength = source.Count - 1;
int dLength = dest.Count;
int s = 0, d = 0;
var sourceVersion = source[0].VersionNumber;
var destVersion = dest[0].VersionNumber;
while (d < dLength)
{
if (sourceVersion.CompareTo(destVersion) >= 0)
{
if (s < sLength)
{
sourceVersion = source[++s].VersionNumber;
}
else
{
// Append all of destination to the end of source.
while (d < dLength)
{
source.Add(dest[d++]);
}
break;
}
}
else
{
source.Insert(s++, dest[d++]);
if (d >= dLength)
{
break;
}
sLength++;
destVersion = dest[d].VersionNumber;
}
}
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default) public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
{ {
var result = new List<PackageInfo>(); var result = new List<PackageInfo>();
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories) foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
{ {
if (repository.Enabled) if (repository.Enabled && repository.Url != null)
{ {
// Where repositories have the same content, the details of the first is taken. // Where repositories have the same content, the details from the first is taken.
foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true)) foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
{ {
if (!Guid.TryParse(package.guid, out var packageGuid)) if (!Guid.TryParse(package.Id, out var packageGuid))
{ {
// Package doesn't have a valid GUID, skip. // Package doesn't have a valid GUID, skip.
continue; continue;
} }
var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault(); var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
// Remove invalid versions from the valid package.
for (var i = package.Versions.Count - 1; i >= 0; i--)
{
var version = package.Versions[i];
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
if (plugin != null)
{
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path);
}
// Remove versions with a target ABI greater then the current application version.
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
{
package.Versions.RemoveAt(i);
}
}
// Don't add a package that doesn't have any compatible versions.
if (package.Versions.Count == 0)
{
continue;
}
if (existing != null) if (existing != null)
{ {
// Assumption is both lists are ordered, so slot these into the correct place. // Assumption is both lists are ordered, so slot these into the correct place.
MergeSort(existing.versions, package.versions); MergeSortedList(existing.Versions, package.Versions);
} }
else else
{ {
@@ -210,23 +229,23 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<PackageInfo> FilterPackages( public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages, IEnumerable<PackageInfo> availablePackages,
string name = null, string? name = null,
Guid guid = default, Guid? id = default,
Version specificVersion = null) Version? specificVersion = null)
{ {
if (name != null) if (name != null)
{ {
availablePackages = availablePackages.Where(x => x.name.Equals(name, StringComparison.OrdinalIgnoreCase)); availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
} }
if (guid != Guid.Empty) if (id != Guid.Empty)
{ {
availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid); availablePackages = availablePackages.Where(x => Guid.Parse(x.Id) == id);
} }
if (specificVersion != null) if (specificVersion != null)
{ {
availablePackages = availablePackages.Where(x => x.versions.Where(y => y.VersionNumber.Equals(specificVersion)).Any()); availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVersion)));
} }
return availablePackages; return availablePackages;
@@ -235,12 +254,12 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<InstallationInfo> GetCompatibleVersions( public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages, IEnumerable<PackageInfo> availablePackages,
string name = null, string? name = null,
Guid guid = default, Guid? id = default,
Version minVersion = null, Version? minVersion = null,
Version specificVersion = null) Version? specificVersion = null)
{ {
var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault(); var package = FilterPackages(availablePackages, name, id, specificVersion).FirstOrDefault();
// Package not found in repository // Package not found in repository
if (package == null) if (package == null)
@@ -249,8 +268,8 @@ namespace Emby.Server.Implementations.Updates
} }
var appVer = _applicationHost.ApplicationVersion; var appVer = _applicationHost.ApplicationVersion;
var availableVersions = package.versions var availableVersions = package.Versions
.Where(x => Version.Parse(x.targetAbi) <= appVer); .Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer);
if (specificVersion != null) if (specificVersion != null)
{ {
@@ -265,12 +284,13 @@ namespace Emby.Server.Implementations.Updates
{ {
yield return new InstallationInfo yield return new InstallationInfo
{ {
Changelog = v.changelog, Changelog = v.Changelog,
Guid = new Guid(package.guid), Id = new Guid(package.Id),
Name = package.name, Name = package.Name,
Version = v.VersionNumber, Version = v.VersionNumber,
SourceUrl = v.sourceUrl, SourceUrl = v.SourceUrl,
Checksum = v.checksum Checksum = v.Checksum,
PackageInfo = package
}; };
} }
} }
@@ -282,20 +302,6 @@ namespace Emby.Server.Implementations.Updates
return GetAvailablePluginUpdates(catalog); return GetAvailablePluginUpdates(catalog);
} }
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
{
var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
foreach (var plugin in plugins)
{
var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
{
yield return version;
}
}
}
/// <inheritdoc /> /// <inheritdoc />
public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken) public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
{ {
@@ -372,143 +378,29 @@ namespace Emby.Server.Implementations.Updates
} }
} }
/// <summary>
/// Installs the package internal.
/// </summary>
/// <param name="package">The package.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
// Set last update time if we were installed before
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
// Do the install
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
// Do plugin-specific processing
_logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
return plugin != null;
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
{
var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;
}
// Always override the passed-in target (which is a file) and figure it out again
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
using var md5 = MD5.Create();
cancellationToken.ThrowIfCancellationRequested();
var hash = Convert.ToHexString(md5.ComputeHash(stream));
if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
"The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
package.Name,
package.Checksum,
hash);
throw new InvalidDataException("The checksum of the received data doesn't match.");
}
// Version folder as they cannot be overwritten in Windows.
targetDir += "_" + package.Version;
if (Directory.Exists(targetDir))
{
try
{
Directory.Delete(targetDir, true);
}
catch
{
// Ignore any exceptions.
}
}
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
#pragma warning restore CA5351
}
/// <summary> /// <summary>
/// Uninstalls a plugin. /// Uninstalls a plugin.
/// </summary> /// </summary>
/// <param name="plugin">The plugin.</param> /// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
public void UninstallPlugin(IPlugin plugin) public void UninstallPlugin(LocalPlugin plugin)
{ {
if (!plugin.CanUninstall) if (plugin == null)
{ {
_logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
return; return;
} }
plugin.OnUninstalling(); if (plugin.Instance?.CanUninstall == false)
{
_logger.LogWarning("Attempt to delete non removable plugin {PluginName}, ignoring request", plugin.Name);
return;
}
plugin.Instance?.OnUninstalling();
// Remove it the quick way for now // Remove it the quick way for now
_applicationHost.RemovePlugin(plugin); _pluginManager.RemovePlugin(plugin);
var path = plugin.AssemblyFilePath; _eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
bool isDirectory = false;
// Check if we have a plugin directory we should remove too
if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath)
{
path = Path.GetDirectoryName(plugin.AssemblyFilePath);
isDirectory = true;
}
// Make this case-insensitive to account for possible incorrect assembly naming
var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
.FirstOrDefault(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(file))
{
path = file;
}
try
{
if (isDirectory)
{
_logger.LogInformation("Deleting plugin directory {0}", path);
Directory.Delete(path, true);
}
else
{
_logger.LogInformation("Deleting plugin file {0}", path);
_fileSystem.DeleteFile(path);
}
}
catch
{
// Ignore file errors.
}
var list = _config.Configuration.UninstalledPlugins.ToList();
var filename = Path.GetFileName(path);
if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase))
{
list.Add(filename);
_config.Configuration.UninstalledPlugins = list.ToArray();
_config.SaveConfiguration();
}
_eventManager.Publish(new PluginUninstalledEventArgs(plugin));
_applicationHost.NotifyPendingRestart(); _applicationHost.NotifyPendingRestart();
} }
@@ -518,7 +410,7 @@ namespace Emby.Server.Implementations.Updates
{ {
lock (_currentInstallationsLock) lock (_currentInstallationsLock)
{ {
var install = _currentInstallations.Find(x => x.info.Guid == id); var install = _currentInstallations.Find(x => x.info.Id == id);
if (install == default((InstallationInfo, CancellationTokenSource))) if (install == default((InstallationInfo, CancellationTokenSource)))
{ {
return false; return false;
@@ -547,14 +439,147 @@ namespace Emby.Server.Implementations.Updates
{ {
lock (_currentInstallationsLock) lock (_currentInstallationsLock)
{ {
foreach (var tuple in _currentInstallations) foreach (var (info, token) in _currentInstallations)
{ {
tuple.token.Dispose(); token.Dispose();
} }
_currentInstallations.Clear(); _currentInstallations.Clear();
} }
} }
} }
/// <summary>
/// Merges two sorted lists.
/// </summary>
/// <param name="source">The source <see cref="IList{VersionInfo}"/> instance to merge.</param>
/// <param name="dest">The destination <see cref="IList{VersionInfo}"/> instance to merge with.</param>
private static void MergeSortedList(IList<VersionInfo> source, IList<VersionInfo> dest)
{
int sLength = source.Count - 1;
int dLength = dest.Count;
int s = 0, d = 0;
var sourceVersion = source[0].VersionNumber;
var destVersion = dest[0].VersionNumber;
while (d < dLength)
{
if (sourceVersion.CompareTo(destVersion) >= 0)
{
if (s < sLength)
{
sourceVersion = source[++s].VersionNumber;
}
else
{
// Append all of destination to the end of source.
while (d < dLength)
{
source.Add(dest[d++]);
}
break;
}
}
else
{
source.Insert(s++, dest[d++]);
if (d >= dLength)
{
break;
}
sLength++;
destVersion = dest[d].VersionNumber;
}
}
}
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
{
var plugins = _pluginManager.Plugins;
foreach (var plugin in plugins)
{
if (plugin.Manifest?.AutoUpdate == false)
{
continue;
}
var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
if (version != null && CompletedInstallations.All(x => x.Id != version.Id))
{
yield return version;
}
}
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
{
var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;
}
// Always override the passed-in target (which is a file) and figure it out again
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
using var md5 = MD5.Create();
cancellationToken.ThrowIfCancellationRequested();
var hash = Convert.ToHexString(md5.ComputeHash(stream));
if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
"The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
package.Name,
package.Checksum,
hash);
throw new InvalidDataException("The checksum of the received data doesn't match.");
}
// Version folder as they cannot be overwritten in Windows.
targetDir += "_" + package.Version;
if (Directory.Exists(targetDir))
{
try
{
Directory.Delete(targetDir, true);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
// Ignore any exceptions.
}
}
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir);
_pluginManager.ImportPluginFrom(targetDir);
}
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
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);
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
return plugin != null;
}
} }
} }

View File

@@ -0,0 +1,28 @@
using System;
namespace Jellyfin.Api.Attributes
{
/// <summary>
/// Internal produces image attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class AcceptsFileAttribute : Attribute
{
private readonly string[] _contentTypes;
/// <summary>
/// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class.
/// </summary>
/// <param name="contentTypes">Content types this endpoint produces.</param>
public AcceptsFileAttribute(params string[] contentTypes)
{
_contentTypes = contentTypes;
}
/// <summary>
/// Gets the configured content types.
/// </summary>
/// <returns>the configured content types.</returns>
public string[] GetContentTypes() => _contentTypes;
}
}

View File

@@ -0,0 +1,18 @@
namespace Jellyfin.Api.Attributes
{
/// <summary>
/// Produces file attribute of "image/*".
/// </summary>
public class AcceptsImageFileAttribute : AcceptsFileAttribute
{
private const string ContentType = "image/*";
/// <summary>
/// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
/// </summary>
public AcceptsImageFileAttribute()
: base(ContentType)
{
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace Jellyfin.Api.Attributes
{
/// <summary>
/// Attribute to mark a parameter as obsolete.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class ParameterObsoleteAttribute : Attribute
{
}
}

View File

@@ -3,6 +3,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.SyncPlay;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -13,20 +14,24 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// </summary> /// </summary>
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement> public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
{ {
private readonly ISyncPlayManager _syncPlayManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class. /// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
/// </summary> /// </summary>
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public SyncPlayAccessHandler( public SyncPlayAccessHandler(
ISyncPlayManager syncPlayManager,
IUserManager userManager, IUserManager userManager,
INetworkManager networkManager, INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor) IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor) : base(userManager, networkManager, httpContextAccessor)
{ {
_syncPlayManager = syncPlayManager;
_userManager = userManager; _userManager = userManager;
} }
@@ -42,10 +47,52 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
var userId = ClaimHelpers.GetUserId(context.User); var userId = ClaimHelpers.GetUserId(context.User);
var user = _userManager.GetUserById(userId!.Value); var user = _userManager.GetUserById(userId!.Value);
if ((requirement.RequiredAccess.HasValue && user.SyncPlayAccess == requirement.RequiredAccess) if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
|| user.SyncPlayAccess == SyncPlayAccess.CreateAndJoinGroups)
{ {
context.Succeed(requirement); if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
|| _syncPlayManager.IsUserActive(userId!.Value))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
{
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
{
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
{
if (_syncPlayManager.IsUserActive(userId!.Value))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
} }
else else
{ {

View File

@@ -11,23 +11,15 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class. /// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
/// </summary> /// </summary>
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccess"/>.</param> /// <param name="requiredAccess">A value of <see cref="SyncPlayAccessRequirementType"/>.</param>
public SyncPlayAccessRequirement(SyncPlayAccess requiredAccess) public SyncPlayAccessRequirement(SyncPlayAccessRequirementType requiredAccess)
{ {
RequiredAccess = requiredAccess; RequiredAccess = requiredAccess;
} }
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
/// </summary>
public SyncPlayAccessRequirement()
{
RequiredAccess = null;
}
/// <summary> /// <summary>
/// Gets the required SyncPlay access. /// Gets the required SyncPlay access.
/// </summary> /// </summary>
public SyncPlayAccess? RequiredAccess { get; } public SyncPlayAccessRequirementType RequiredAccess { get; }
} }
} }

View File

@@ -51,13 +51,23 @@ namespace Jellyfin.Api.Constants
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl"; public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
/// <summary> /// <summary>
/// Policy name for requiring access to SyncPlay. /// Policy name for accessing SyncPlay.
/// </summary> /// </summary>
public const string SyncPlayAccess = "SyncPlayAccess"; public const string SyncPlayHasAccess = "SyncPlayHasAccess";
/// <summary> /// <summary>
/// Policy name for requiring group creation access to SyncPlay. /// Policy name for creating a SyncPlay group.
/// </summary> /// </summary>
public const string SyncPlayCreateGroupAccess = "SyncPlayCreateGroupAccess"; public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
/// <summary>
/// Policy name for joining a SyncPlay group.
/// </summary>
public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
/// <summary>
/// Policy name for accessing a SyncPlay group.
/// </summary>
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
} }
} }

View File

@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -174,7 +174,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,
@@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -339,7 +339,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,

View File

@@ -1,13 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@@ -121,9 +120,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)

View File

@@ -29,18 +29,22 @@ namespace Jellyfin.Api.Controllers
{ {
private readonly ILogger<DashboardController> _logger; private readonly ILogger<DashboardController> _logger;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IPluginManager _pluginManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> class. /// Initializes a new instance of the <see cref="DashboardController"/> class.
/// </summary> /// </summary>
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param> /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
public DashboardController( public DashboardController(
ILogger<DashboardController> logger, ILogger<DashboardController> logger,
IServerApplicationHost appHost) IServerApplicationHost appHost,
IPluginManager pluginManager)
{ {
_logger = logger; _logger = logger;
_appHost = appHost; _appHost = appHost;
_pluginManager = pluginManager;
} }
/// <summary> /// <summary>
@@ -83,7 +87,7 @@ namespace Jellyfin.Api.Controllers
.Where(i => i != null) .Where(i => i != null)
.ToList(); .ToList();
configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages));
if (pageType.HasValue) if (pageType.HasValue)
{ {
@@ -155,24 +159,24 @@ namespace Jellyfin.Api.Controllers
return NotFound(); return NotFound();
} }
private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin) private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
{ {
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
} }
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin) private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin)
{ {
if (!(plugin is IHasWebPages hasWebPages)) if (plugin?.Instance is not IHasWebPages hasWebPages)
{ {
return new List<Tuple<PluginPageInfo, IPlugin>>(); return new List<Tuple<PluginPageInfo, IPlugin>>();
} }
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin)); return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
} }
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages() private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
{ {
return _appHost.Plugins.SelectMany(GetPluginPages); return _pluginManager.Plugins.SelectMany(GetPluginPages);
} }
} }
} }

View File

@@ -12,6 +12,7 @@ using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers namespace Jellyfin.Api.Controllers
{ {
@@ -22,14 +23,17 @@ namespace Jellyfin.Api.Controllers
public class DisplayPreferencesController : BaseJellyfinApiController public class DisplayPreferencesController : BaseJellyfinApiController
{ {
private readonly IDisplayPreferencesManager _displayPreferencesManager; private readonly IDisplayPreferencesManager _displayPreferencesManager;
private readonly ILogger<DisplayPreferencesController> _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class. /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
/// </summary> /// </summary>
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param> /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager) /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
{ {
_displayPreferencesManager = displayPreferencesManager; _displayPreferencesManager = displayPreferencesManager;
_logger = logger;
} }
/// <summary> /// <summary>
@@ -61,7 +65,6 @@ namespace Jellyfin.Api.Controllers
{ {
Client = displayPreferences.Client, Client = displayPreferences.Client,
Id = displayPreferences.ItemId.ToString(), Id = displayPreferences.ItemId.ToString(),
ViewType = itemPreferences.ViewType.ToString(),
SortBy = itemPreferences.SortBy, SortBy = itemPreferences.SortBy,
SortOrder = itemPreferences.SortOrder, SortOrder = itemPreferences.SortOrder,
IndexBy = displayPreferences.IndexBy?.ToString(), IndexBy = displayPreferences.IndexBy?.ToString(),
@@ -77,16 +80,12 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant(); dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
} }
foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
{
dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
}
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant(); dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome; dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme;
// Load all custom display preferences // Load all custom display preferences
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
@@ -189,10 +188,9 @@ namespace Jellyfin.Api.Controllers
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase))) foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{ {
if (Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var preferenceId)) if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
{ {
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, preferenceId, existingDisplayPreferences.Client); _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
displayPreferences.CustomPrefs.Remove(key); displayPreferences.CustomPrefs.Remove(key);
} }
} }
@@ -204,11 +202,6 @@ namespace Jellyfin.Api.Controllers
itemPrefs.RememberSorting = displayPreferences.RememberSorting; itemPrefs.RememberSorting = displayPreferences.RememberSorting;
itemPrefs.ItemId = itemId; itemPrefs.ItemId = itemId;
if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
{
itemPrefs.ViewType = viewType;
}
// Set all remaining custom preferences. // Set all remaining custom preferences.
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs); _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
_displayPreferencesManager.SaveChanges(); _displayPreferencesManager.SaveChanges();

View File

@@ -41,18 +41,25 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Description xml returned.</response> /// <response code="200">Description xml returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns> /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
[HttpGet("{serverId}/description")] [HttpGet("{serverId}/description")]
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
{ {
var url = GetAbsoluteUri(); if (DlnaEntryPoint.Enabled)
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); {
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); var url = GetAbsoluteUri();
return Ok(xml); var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
return Ok(xml);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -60,17 +67,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna content directory returned.</response> /// <response code="200">Dlna content directory returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
[HttpGet("{serverId}/ContentDirectory")] [HttpGet("{serverId}/ContentDirectory")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetContentDirectory([FromRoute, Required] string serverId) public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
{ {
return Ok(_contentDirectory.GetServiceXml()); if (DlnaEntryPoint.Enabled)
{
return Ok(_contentDirectory.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -78,17 +92,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response> /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Dlna media receiver registrar xml.</returns> /// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar")] [HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{ {
return Ok(_mediaReceiverRegistrar.GetServiceXml()); if (DlnaEntryPoint.Enabled)
{
return Ok(_mediaReceiverRegistrar.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -96,17 +117,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response> /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Dlna media receiver registrar xml.</returns> /// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager")] [HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetConnectionManager([FromRoute, Required] string serverId) public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
{ {
return Ok(_connectionManager.GetServiceXml()); if (DlnaEntryPoint.Enabled)
{
return Ok(_connectionManager.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -114,14 +142,21 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns> /// <returns>Control response.</returns>
[HttpPost("{serverId}/ContentDirectory/Control")] [HttpPost("{serverId}/ContentDirectory/Control")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
{ {
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); if (DlnaEntryPoint.Enabled)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -129,14 +164,21 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns> /// <returns>Control response.</returns>
[HttpPost("{serverId}/ConnectionManager/Control")] [HttpPost("{serverId}/ConnectionManager/Control")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
{ {
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); if (DlnaEntryPoint.Enabled)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -144,14 +186,21 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns> /// <returns>Control response.</returns>
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")] [HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
{ {
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); if (DlnaEntryPoint.Enabled)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -159,17 +208,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns> /// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")] [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")] [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
{ {
return ProcessEventRequest(_mediaReceiverRegistrar); if (DlnaEntryPoint.Enabled)
{
return ProcessEventRequest(_mediaReceiverRegistrar);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -177,17 +233,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns> /// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ContentDirectory/Events")] [HttpSubscribe("{serverId}/ContentDirectory/Events")]
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")] [HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
{ {
return ProcessEventRequest(_contentDirectory); if (DlnaEntryPoint.Enabled)
{
return ProcessEventRequest(_contentDirectory);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -195,17 +258,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response> /// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns> /// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ConnectionManager/Events")] [HttpSubscribe("{serverId}/ConnectionManager/Events")]
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")] [HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
{ {
return ProcessEventRequest(_connectionManager); if (DlnaEntryPoint.Enabled)
{
return ProcessEventRequest(_connectionManager);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -213,14 +283,24 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <param name="fileName">The icon filename.</param> /// <param name="fileName">The icon filename.</param>
/// <response code="200">Request processed.</response>
/// <response code="404">Not Found.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Icon stream.</returns> /// <returns>Icon stream.</returns>
[HttpGet("{serverId}/icons/{fileName}")] [HttpGet("{serverId}/icons/{fileName}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile] [ProducesImageFile]
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
{ {
return GetIconInternal(fileName); if (DlnaEntryPoint.Enabled)
{
return GetIconInternal(fileName);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@@ -228,11 +308,22 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="fileName">The icon filename.</param> /// <param name="fileName">The icon filename.</param>
/// <returns>Icon stream.</returns> /// <returns>Icon stream.</returns>
/// <response code="200">Request processed.</response>
/// <response code="404">Not Found.</response>
/// <response code="503">DLNA is disabled.</response>
[HttpGet("icons/{fileName}")] [HttpGet("icons/{fileName}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile] [ProducesImageFile]
public ActionResult GetIcon([FromRoute, Required] string fileName) public ActionResult GetIcon([FromRoute, Required] string fileName)
{ {
return GetIconInternal(fileName); if (DlnaEntryPoint.Enabled)
{
return GetIconInternal(fileName);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
private ActionResult GetIconInternal(string fileName) private ActionResult GetIconInternal(string fileName)

View File

@@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -219,7 +219,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions, [FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true) [FromQuery] bool enableAdaptiveBitrateStreaming = true)
{ {
@@ -256,7 +256,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,
@@ -271,7 +271,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions, StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
@@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -386,7 +386,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions, [FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true) [FromQuery] bool enableAdaptiveBitrateStreaming = true)
{ {
@@ -423,7 +423,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,
@@ -438,7 +438,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions, StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
@@ -534,7 +534,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -549,7 +549,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
@@ -586,7 +586,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,
@@ -601,7 +601,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions StreamOptions = streamOptions
}; };
@@ -699,7 +699,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -714,7 +714,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
@@ -751,7 +751,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,
@@ -766,7 +766,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions StreamOptions = streamOptions
}; };
@@ -869,7 +869,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -884,7 +884,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
var streamingRequest = new VideoRequestDto var streamingRequest = new VideoRequestDto
@@ -921,7 +921,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,
@@ -936,7 +936,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions StreamOptions = streamOptions
}; };
@@ -1041,7 +1041,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -1056,7 +1056,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
var streamingRequest = new StreamingRequestDto var streamingRequest = new StreamingRequestDto
@@ -1093,7 +1093,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,
@@ -1108,7 +1108,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions StreamOptions = streamOptions
}; };

View File

@@ -98,7 +98,9 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Videos/ActiveEncodings")] [HttpDelete("Videos/ActiveEncodings")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [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); _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
return NoContent(); return NoContent();

View File

@@ -87,6 +87,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")] [HttpPost("Users/{userId}/Images/{imageType}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -98,7 +99,7 @@ namespace Jellyfin.Api.Controllers
{ {
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{ {
return Forbid("User is not allowed to update the image."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
} }
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@@ -133,6 +134,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}/{index}")] [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -144,7 +146,7 @@ namespace Jellyfin.Api.Controllers
{ {
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{ {
return Forbid("User is not allowed to update the image."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
} }
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@@ -190,7 +192,7 @@ namespace Jellyfin.Api.Controllers
{ {
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{ {
return Forbid("User is not allowed to delete the image."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
} }
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@@ -229,7 +231,7 @@ namespace Jellyfin.Api.Controllers
{ {
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{ {
return Forbid("User is not allowed to delete the image."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
} }
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@@ -312,6 +314,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns> /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpPost("Items/{itemId}/Images/{imageType}")] [HttpPost("Items/{itemId}/Images/{imageType}")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@@ -325,9 +328,11 @@ namespace Jellyfin.Api.Controllers
return NotFound(); return NotFound();
} }
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8 // Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault(); var mimeType = Request.ContentType.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
@@ -344,6 +349,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns> /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")] [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@@ -358,9 +364,11 @@ namespace Jellyfin.Api.Controllers
return NotFound(); return NotFound();
} }
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8 // Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault(); var mimeType = Request.ContentType.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
@@ -384,7 +392,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex, [FromRoute, Required] int imageIndex,
[FromQuery] int newIndex) [FromQuery, Required] int newIndex)
{ {
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
if (item == null) if (item == null)
@@ -733,7 +741,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetArtistImage( public async Task<ActionResult> GetArtistImage(
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromQuery] string tag, [FromQuery] string? tag,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
@@ -812,7 +820,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetGenreImage( public async Task<ActionResult> GetGenreImage(
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromQuery] string tag, [FromQuery] string? tag,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
@@ -892,7 +900,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex, [FromRoute, Required] int imageIndex,
[FromQuery] string tag, [FromQuery] string? tag,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
@@ -970,7 +978,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetMusicGenreImage( public async Task<ActionResult> GetMusicGenreImage(
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromQuery] string tag, [FromQuery] string? tag,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
@@ -1050,7 +1058,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex, [FromRoute, Required] int imageIndex,
[FromQuery] string tag, [FromQuery] string? tag,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
@@ -1128,7 +1136,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetPersonImage( public async Task<ActionResult> GetPersonImage(
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromQuery] string tag, [FromQuery] string? tag,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
@@ -1208,7 +1216,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex, [FromRoute, Required] int imageIndex,
[FromQuery] string tag, [FromQuery] string? tag,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,

View File

@@ -346,11 +346,12 @@ namespace Jellyfin.Api.Controllers
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
using (var stream = result.Content) using (var stream = result.Content)
{ {
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var fileStream = new FileStream( await using var fileStream = new FileStream(
fullCachePath, fullCachePath,
FileMode.Create, FileMode.Create,
FileAccess.Write, FileAccess.Write,
FileShare.Read, FileShare.None,
IODefaults.FileStreamBufferSize, IODefaults.FileStreamBufferSize,
true); true);

View File

@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}/ContentType")] [HttpPost("Items/{itemId}/ContentType")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [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); var item = _libraryManager.GetItemById(itemId);
if (item == null) if (item == null)

View File

@@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? recursive, [FromQuery] bool? recursive,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
@@ -184,7 +184,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -254,18 +254,18 @@ namespace Jellyfin.Api.Controllers
includeItemTypes = new[] { "Playlist" }; includeItemTypes = new[] { "Playlist" };
} }
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
// Assume all folders inside an EnabledChannel are enabled // Assume all folders inside an EnabledChannel are enabled
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id) || Array.IndexOf(enabledChannels, item.Id) != -1
// Assume all items inside an EnabledChannel are enabled // Assume all items inside an EnabledChannel are enabled
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId); || Array.IndexOf(enabledChannels, item.ChannelId) != -1;
var collectionFolders = _libraryManager.GetCollectionFolders(item); var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders) foreach (var collectionFolder in collectionFolders)
{ {
if (user.GetPreference(PreferenceKind.EnabledFolders).Contains( if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
StringComparer.OrdinalIgnoreCase))
{ {
isInEnabledFolder = true; isInEnabledFolder = true;
} }
@@ -608,7 +608,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? recursive, [FromQuery] bool? recursive,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
@@ -617,7 +617,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -786,12 +786,12 @@ namespace Jellyfin.Api.Controllers
var ancestorIds = Array.Empty<Guid>(); var ancestorIds = Array.Empty<Guid>();
var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes); var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0) if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
{ {
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder) .Where(i => i is Folder)
.Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) .Where(i => !excludeFolderIds.Contains(i.Id))
.Select(i => i.Id) .Select(i => i.Id)
.ToArray(); .ToArray();
} }

View File

@@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers
} }
// TODO determine non-ASCII validity. // TODO determine non-ASCII validity.
return PhysicalFile(path, MimeTypes.GetMimeType(path)); return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
} }
/// <summary> /// <summary>
@@ -742,8 +742,6 @@ namespace Jellyfin.Api.Controllers
{ {
Limit = limit, Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(), IncludeItemTypes = includeItemTypes.ToArray(),
IsMovie = isMovie,
IsSeries = isSeries,
SimilarTo = item, SimilarTo = item,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true, EnableTotalRecordCount = !isMovie ?? true,
@@ -780,7 +778,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo( public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
[FromQuery] string? libraryContentType, [FromQuery] string? libraryContentType,
[FromQuery] bool isNewLibrary) [FromQuery] bool isNewLibrary = false)
{ {
var result = new LibraryOptionsResultDto(); var result = new LibraryOptionsResultDto();

View File

@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddVirtualFolder( public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string? name, [FromQuery] string? name,
[FromQuery] string? collectionType, [FromQuery] CollectionTypeOptions? collectionType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromBody] AddVirtualFolderDto? libraryOptionsDto,
[FromQuery] bool refreshLibrary = false) [FromQuery] bool refreshLibrary = false)
@@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Updates a media path. /// Updates a media path.
/// </summary> /// </summary>
/// <param name="name">The name of the library.</param> /// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
/// <param name="pathInfo">The path info.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path updated.</response> /// <response code="204">Media path updated.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
[HttpPost("Paths/Update")] [HttpPost("Paths/Update")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaPath( public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
[FromQuery] string? name,
[FromBody] MediaPathInfo? pathInfo)
{ {
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(); return NoContent();
} }

View File

@@ -553,8 +553,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isSports, [FromQuery] bool? isSports,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] string? sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
@@ -1119,20 +1119,15 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Set channel mappings. /// Set channel mappings.
/// </summary> /// </summary>
/// <param name="providerId">Provider id.</param> /// <param name="setChannelMappingDto">The set channel mapping dto.</param>
/// <param name="tunerChannelId">Tuner channel id.</param>
/// <param name="providerChannelId">Provider channel id.</param>
/// <response code="200">Created channel mapping returned.</response> /// <response code="200">Created channel mapping returned.</response>
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns> /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
[HttpPost("ChannelMappings")] [HttpPost("ChannelMappings")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping( public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
[FromQuery] string? providerId,
[FromQuery] string? tunerChannelId,
[FromQuery] string? providerChannelId)
{ {
return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false); return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
} }
/// <summary> /// <summary>

View File

@@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers namespace Jellyfin.Api.Controllers
@@ -82,6 +83,7 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
/// Query parameters are obsolete.
/// </remarks> /// </remarks>
/// <param name="itemId">The item id.</param> /// <param name="itemId">The item id.</param>
/// <param name="userId">The user id.</param> /// <param name="userId">The user id.</param>
@@ -105,21 +107,21 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo( public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId, [FromQuery, ParameterObsolete] Guid? userId,
[FromQuery] int? maxStreamingBitrate, [FromQuery, ParameterObsolete] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks, [FromQuery, ParameterObsolete] long? startTimeTicks,
[FromQuery] int? audioStreamIndex, [FromQuery, ParameterObsolete] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex, [FromQuery, ParameterObsolete] int? subtitleStreamIndex,
[FromQuery] int? maxAudioChannels, [FromQuery, ParameterObsolete] int? maxAudioChannels,
[FromQuery] string? mediaSourceId, [FromQuery, ParameterObsolete] string? mediaSourceId,
[FromQuery] string? liveStreamId, [FromQuery, ParameterObsolete] string? liveStreamId,
[FromQuery] bool? autoOpenLiveStream, [FromQuery, ParameterObsolete] bool? autoOpenLiveStream,
[FromQuery] bool? enableDirectPlay, [FromQuery, ParameterObsolete] bool? enableDirectPlay,
[FromQuery] bool? enableDirectStream, [FromQuery, ParameterObsolete] bool? enableDirectStream,
[FromQuery] bool? enableTranscoding, [FromQuery, ParameterObsolete] bool? enableTranscoding,
[FromQuery] bool? allowVideoStreamCopy, [FromQuery, ParameterObsolete] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy, [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
[FromBody] PlaybackInfoDto? playbackInfoDto) [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
{ {
var authInfo = _authContext.GetAuthorizationInfo(Request); var authInfo = _authContext.GetAuthorizationInfo(Request);
@@ -258,24 +260,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] int? maxAudioChannels, [FromQuery] int? maxAudioChannels,
[FromQuery] Guid? itemId, [FromQuery] Guid? itemId,
[FromBody] OpenLiveStreamDto openLiveStreamDto, [FromBody] OpenLiveStreamDto? openLiveStreamDto,
[FromQuery] bool enableDirectPlay = true, [FromQuery] bool? enableDirectPlay,
[FromQuery] bool enableDirectStream = true) [FromQuery] bool? enableDirectStream)
{ {
var request = new LiveStreamRequest var request = new LiveStreamRequest
{ {
OpenToken = openToken, OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
UserId = userId ?? Guid.Empty, UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
PlaySessionId = playSessionId, PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
MaxStreamingBitrate = maxStreamingBitrate, MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
ItemId = itemId ?? Guid.Empty, ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty,
DeviceProfile = openLiveStreamDto?.DeviceProfile, DeviceProfile = openLiveStreamDto?.DeviceProfile,
EnableDirectPlay = enableDirectPlay, EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
EnableDirectStream = enableDirectStream, EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
}; };
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false); return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);

View File

@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates; using MediaBrowser.Model.Updates;
@@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
if (!string.IsNullOrEmpty(repositoryUrl)) if (!string.IsNullOrEmpty(repositoryUrl))
{ {
packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any()) packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
.ToList(); .ToList();
} }
@@ -157,7 +158,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Repositories")] [HttpPost("Repositories")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos) public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
{ {
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos; _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
_serverConfigurationManager.SaveConfiguration(); _serverConfigurationManager.SaveConfiguration();

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
@@ -17,6 +19,7 @@ using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Jellyfin.Api.Controllers namespace Jellyfin.Api.Controllers
{ {
@@ -53,6 +56,14 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Creates a new playlist. /// Creates a new playlist.
/// </summary> /// </summary>
/// <remarks>
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
/// Query parameters are obsolete.
/// </remarks>
/// <param name="name">The playlist name.</param>
/// <param name="ids">The item ids.</param>
/// <param name="userId">The user id.</param>
/// <param name="mediaType">The media type.</param>
/// <param name="createPlaylistRequest">The create playlist payload.</param> /// <param name="createPlaylistRequest">The create playlist payload.</param>
/// <returns> /// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
@@ -61,14 +72,23 @@ namespace Jellyfin.Api.Controllers
[HttpPost] [HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromBody, Required] CreatePlaylistDto createPlaylistRequest) [FromQuery, ParameterObsolete] string? name,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
[FromQuery, ParameterObsolete] Guid? userId,
[FromQuery, ParameterObsolete] string? mediaType,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
{ {
if (ids.Count == 0)
{
ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
}
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{ {
Name = createPlaylistRequest.Name, Name = name ?? createPlaylistRequest?.Name,
ItemIdList = createPlaylistRequest.Ids, ItemIdList = ids,
UserId = createPlaylistRequest.UserId, UserId = userId ?? createPlaylistRequest?.UserId ?? default,
MediaType = createPlaylistRequest.MediaType MediaType = mediaType ?? createPlaylistRequest?.MediaType
}).ConfigureAwait(false); }).ConfigureAwait(false);
return result; return result;

View File

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

View File

@@ -1,15 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.PluginDtos; using Jellyfin.Api.Models.PluginDtos;
using MediaBrowser.Common; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json; using MediaBrowser.Common.Json;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -23,112 +29,26 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
public class PluginsController : BaseJellyfinApiController public class PluginsController : BaseJellyfinApiController
{ {
private readonly IApplicationHost _appHost;
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;
private readonly IPluginManager _pluginManager;
private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); private readonly IConfigurationManager _config;
private readonly JsonSerializerOptions _serializerOptions;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PluginsController"/> class. /// Initializes a new instance of the <see cref="PluginsController"/> class.
/// </summary> /// </summary>
/// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
/// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
public PluginsController( public PluginsController(
IApplicationHost appHost, IInstallationManager installationManager,
IInstallationManager installationManager) IPluginManager pluginManager,
IConfigurationManager config)
{ {
_appHost = appHost;
_installationManager = installationManager; _installationManager = installationManager;
} _pluginManager = pluginManager;
_serializerOptions = JsonDefaults.GetOptions();
/// <summary> _config = config;
/// Gets a list of currently installed plugins.
/// </summary>
/// <response code="200">Installed plugins returned.</response>
/// <returns>List of currently installed plugins.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
{
return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
[HttpDelete("{pluginId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
{
var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
if (plugin == null)
{
return NotFound();
}
_installationManager.UninstallPlugin(plugin);
return NoContent();
}
/// <summary>
/// Gets plugin configuration.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="200">Plugin configuration returned.</response>
/// <response code="404">Plugin not found or plugin configuration not found.</response>
/// <returns>Plugin configuration.</returns>
[HttpGet("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
return NotFound();
}
return plugin.Configuration;
}
/// <summary>
/// Updates plugin configuration.
/// </summary>
/// <remarks>
/// Accepts plugin configuration as JSON body.
/// </remarks>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin configuration updated.</response>
/// <response code="404">Plugin not found or plugin does not have configuration.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
/// The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/>
/// when plugin not found or plugin doesn't have configuration.
/// </returns>
[HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
return NotFound();
}
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
.ConfigureAwait(false);
if (configuration != null)
{
plugin.UpdateConfiguration(configuration);
}
return NoContent();
} }
/// <summary> /// <summary>
@@ -139,7 +59,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("This endpoint should not be used.")] [Obsolete("This endpoint should not be used.")]
[HttpGet("SecurityInfo")] [HttpGet("SecurityInfo")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo() public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
{ {
return new PluginSecurityInfo return new PluginSecurityInfo
{ {
@@ -148,21 +68,6 @@ namespace Jellyfin.Api.Controllers
}; };
} }
/// <summary>
/// Updates plugin security info.
/// </summary>
/// <param name="pluginSecurityInfo">Plugin security info.</param>
/// <response code="204">Plugin security info updated.</response>
/// <returns>An <see cref="NoContentResult"/>.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("SecurityInfo")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
{
return NoContent();
}
/// <summary> /// <summary>
/// Gets registration status for a feature. /// Gets registration status for a feature.
/// </summary> /// </summary>
@@ -172,7 +77,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("This endpoint should not be used.")] [Obsolete("This endpoint should not be used.")]
[HttpPost("RegistrationRecords/{name}")] [HttpPost("RegistrationRecords/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name) public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
{ {
return new MBRegistrationRecord return new MBRegistrationRecord
{ {
@@ -194,11 +99,251 @@ namespace Jellyfin.Api.Controllers
[Obsolete("Paid plugins are not supported")] [Obsolete("Paid plugins are not supported")]
[HttpGet("Registrations/{name}")] [HttpGet("Registrations/{name}")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)] [ProducesResponseType(StatusCodes.Status501NotImplemented)]
public ActionResult GetRegistration([FromRoute, Required] string name) public static ActionResult GetRegistration([FromRoute, Required] string name)
{ {
// TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
// delete all these registration endpoints. They are only kept for compatibility. // delete all these registration endpoints. They are only kept for compatibility.
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <summary>
/// Gets a list of currently installed plugins.
/// </summary>
/// <response code="200">Installed plugins returned.</response>
/// <returns>List of currently installed plugins.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
{
return Ok(_pluginManager.Plugins
.OrderBy(p => p.Name)
.Select(p => p.GetPluginInfo()));
}
/// <summary>
/// Enables a disabled plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin enabled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/{version}/Enable")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{
return NotFound();
}
_pluginManager.EnablePlugin(plugin);
return NoContent();
}
/// <summary>
/// Disable a plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin disabled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/{version}/Disable")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{
return NotFound();
}
_pluginManager.DisablePlugin(plugin);
return NoContent();
}
/// <summary>
/// Uninstalls a plugin by version.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpDelete("{pluginId}/{version}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{
return NotFound();
}
_installationManager.UninstallPlugin(plugin);
return NoContent();
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpDelete("{pluginId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Please use the UninstallPluginByVersion API.")]
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
{
// If no version is given, return the current instance.
var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId));
// Select the un-instanced one first.
var plugin = plugins.FirstOrDefault(p => p.Instance == null);
if (plugin == null)
{
// Then by the status.
plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
}
if (plugin != null)
{
_installationManager.UninstallPlugin(plugin);
return NoContent();
}
return NotFound();
}
/// <summary>
/// Gets plugin configuration.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="200">Plugin configuration returned.</response>
/// <response code="404">Plugin not found or plugin configuration not found.</response>
/// <returns>Plugin configuration.</returns>
[HttpGet("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
{
var plugin = _pluginManager.GetPlugin(pluginId);
if (plugin?.Instance is IHasPluginConfiguration configPlugin)
{
return configPlugin.Configuration;
}
return NotFound();
}
/// <summary>
/// Updates plugin configuration.
/// </summary>
/// <remarks>
/// Accepts plugin configuration as JSON body.
/// </remarks>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin configuration updated.</response>
/// <response code="404">Plugin not found or plugin does not have configuration.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
{
var plugin = _pluginManager.GetPlugin(pluginId);
if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
{
return NotFound();
}
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
.ConfigureAwait(false);
if (configuration != null)
{
configPlugin.UpdateConfiguration(configuration);
}
return NoContent();
}
/// <summary>
/// Gets a plugin's image.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="200">Plugin image returned.</response>
/// <returns>Plugin's image.</returns>
[HttpGet("{pluginId}/{version}/Image")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
[AllowAnonymous]
public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{
return NotFound();
}
var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
if (plugin.Manifest.ImagePath == null || !System.IO.File.Exists(imagePath))
{
return NotFound();
}
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
}
/// <summary>
/// Gets a plugin's manifest.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin manifest returned.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>A <see cref="PluginManifest"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/Manifest")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<PluginManifest> GetPluginManifest([FromRoute, Required] Guid pluginId)
{
var plugin = _pluginManager.GetPlugin(pluginId);
if (plugin != null)
{
return plugin.Manifest;
}
return NotFound();
}
/// <summary>
/// Updates plugin security info.
/// </summary>
/// <param name="pluginSecurityInfo">Plugin security info.</param>
/// <response code="204">Plugin security info updated.</response>
/// <returns>An <see cref="NoContentResult"/>.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("SecurityInfo")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
{
return NoContent();
}
} }
} }

View File

@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
{ {
if (_quickConnect.State == QuickConnectState.Unavailable) if (_quickConnect.State == QuickConnectState.Unavailable)
{ {
return Forbid("Quick connect is unavailable"); return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
} }
_quickConnect.Activate(); _quickConnect.Activate();
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
if (!userId.HasValue) if (!userId.HasValue)
{ {
return Forbid("Unknown user id"); return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
} }
return _quickConnect.AuthorizeRequest(userId.Value, code); return _quickConnect.AuthorizeRequest(userId.Value, code);

View File

@@ -259,7 +259,8 @@ namespace Jellyfin.Api.Controllers
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(fullCacheDirectory); 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); await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath)); var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));

View File

@@ -371,6 +371,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="204">Subtitle uploaded.</response> /// <response code="204">Subtitle uploaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Videos/{itemId}/Subtitles")] [HttpPost("Videos/{itemId}/Subtitles")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UploadSubtitle( public async Task<ActionResult> UploadSubtitle(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,

View File

@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// The sync play controller. /// The sync play controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.SyncPlayAccess)] [Authorize(Policy = Policies.SyncPlayHasAccess)]
public class SyncPlayController : BaseJellyfinApiController public class SyncPlayController : BaseJellyfinApiController
{ {
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
@@ -51,7 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("New")] [HttpPost("New")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayCreateGroupAccess)] [Authorize(Policy = Policies.SyncPlayCreateGroup)]
public ActionResult SyncPlayCreateGroup( public ActionResult SyncPlayCreateGroup(
[FromBody, Required] NewGroupRequestDto requestData) [FromBody, Required] NewGroupRequestDto requestData)
{ {
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Join")] [HttpPost("Join")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayAccess)] [Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult SyncPlayJoinGroup( public ActionResult SyncPlayJoinGroup(
[FromBody, Required] JoinGroupRequestDto requestData) [FromBody, Required] JoinGroupRequestDto requestData)
{ {
@@ -86,6 +86,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Leave")] [HttpPost("Leave")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayLeaveGroup() public ActionResult SyncPlayLeaveGroup()
{ {
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns> /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
[HttpGet("List")] [HttpGet("List")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.SyncPlayAccess)] [Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups() public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
{ {
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -117,6 +118,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetNewQueue")] [HttpPost("SetNewQueue")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetNewQueue( public ActionResult SyncPlaySetNewQueue(
[FromBody, Required] PlayRequestDto requestData) [FromBody, Required] PlayRequestDto requestData)
{ {
@@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetPlaylistItem")] [HttpPost("SetPlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetPlaylistItem( public ActionResult SyncPlaySetPlaylistItem(
[FromBody, Required] SetPlaylistItemRequestDto requestData) [FromBody, Required] SetPlaylistItemRequestDto requestData)
{ {
@@ -154,6 +157,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoveFromPlaylist")] [HttpPost("RemoveFromPlaylist")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayRemoveFromPlaylist( public ActionResult SyncPlayRemoveFromPlaylist(
[FromBody, Required] RemoveFromPlaylistRequestDto requestData) [FromBody, Required] RemoveFromPlaylistRequestDto requestData)
{ {
@@ -171,6 +175,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("MovePlaylistItem")] [HttpPost("MovePlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayMovePlaylistItem( public ActionResult SyncPlayMovePlaylistItem(
[FromBody, Required] MovePlaylistItemRequestDto requestData) [FromBody, Required] MovePlaylistItemRequestDto requestData)
{ {
@@ -188,6 +193,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Queue")] [HttpPost("Queue")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayQueue( public ActionResult SyncPlayQueue(
[FromBody, Required] QueueRequestDto requestData) [FromBody, Required] QueueRequestDto requestData)
{ {
@@ -204,6 +210,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Unpause")] [HttpPost("Unpause")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayUnpause() public ActionResult SyncPlayUnpause()
{ {
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -219,6 +226,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Pause")] [HttpPost("Pause")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPause() public ActionResult SyncPlayPause()
{ {
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -234,6 +242,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Stop")] [HttpPost("Stop")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayStop() public ActionResult SyncPlayStop()
{ {
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -250,6 +259,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Seek")] [HttpPost("Seek")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySeek( public ActionResult SyncPlaySeek(
[FromBody, Required] SeekRequestDto requestData) [FromBody, Required] SeekRequestDto requestData)
{ {
@@ -267,6 +277,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Buffering")] [HttpPost("Buffering")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayBuffering( public ActionResult SyncPlayBuffering(
[FromBody, Required] BufferRequestDto requestData) [FromBody, Required] BufferRequestDto requestData)
{ {
@@ -288,6 +299,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Ready")] [HttpPost("Ready")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayReady( public ActionResult SyncPlayReady(
[FromBody, Required] ReadyRequestDto requestData) [FromBody, Required] ReadyRequestDto requestData)
{ {
@@ -309,6 +321,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetIgnoreWait")] [HttpPost("SetIgnoreWait")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetIgnoreWait( public ActionResult SyncPlaySetIgnoreWait(
[FromBody, Required] IgnoreWaitRequestDto requestData) [FromBody, Required] IgnoreWaitRequestDto requestData)
{ {
@@ -326,6 +339,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("NextItem")] [HttpPost("NextItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayNextItem( public ActionResult SyncPlayNextItem(
[FromBody, Required] NextItemRequestDto requestData) [FromBody, Required] NextItemRequestDto requestData)
{ {
@@ -343,6 +357,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("PreviousItem")] [HttpPost("PreviousItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPreviousItem( public ActionResult SyncPlayPreviousItem(
[FromBody, Required] PreviousItemRequestDto requestData) [FromBody, Required] PreviousItemRequestDto requestData)
{ {
@@ -360,6 +375,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetRepeatMode")] [HttpPost("SetRepeatMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetRepeatMode( public ActionResult SyncPlaySetRepeatMode(
[FromBody, Required] SetRepeatModeRequestDto requestData) [FromBody, Required] SetRepeatModeRequestDto requestData)
{ {
@@ -377,6 +393,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetShuffleMode")] [HttpPost("SetShuffleMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetShuffleMode( public ActionResult SyncPlaySetShuffleMode(
[FromBody, Required] SetShuffleModeRequestDto requestData) [FromBody, Required] SetShuffleModeRequestDto requestData)
{ {

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Mime; using System.Net.Mime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -66,7 +67,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<SystemInfo> GetSystemInfo() public ActionResult<SystemInfo> GetSystemInfo()
{ {
return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
} }
/// <summary> /// <summary>
@@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo() public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{ {
return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
} }
/// <summary> /// <summary>

View File

@@ -1,6 +1,7 @@
using System; using System;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
@@ -144,7 +145,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? recursive, [FromQuery] bool? recursive,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
@@ -152,7 +153,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,

View File

@@ -67,6 +67,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param> /// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
[HttpGet("NextUp")] [HttpGet("NextUp")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
@@ -81,7 +82,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool disableFirstEpisode = false)
{ {
var options = new DtoOptions { Fields = fields } var options = new DtoOptions { Fields = fields }
.AddClientFields(Request) .AddClientFields(Request)
@@ -95,7 +97,8 @@ namespace Jellyfin.Api.Controllers
SeriesId = seriesId, SeriesId = seriesId,
StartIndex = startIndex, StartIndex = startIndex,
UserId = userId ?? Guid.Empty, UserId = userId ?? Guid.Empty,
EnableTotalRecordCount = enableTotalRecordCount EnableTotalRecordCount = enableTotalRecordCount,
DisableFirstEpisode = disableFirstEpisode
}, },
options); options);
@@ -267,7 +270,7 @@ namespace Jellyfin.Api.Controllers
if (startItemId.HasValue) if (startItemId.HasValue)
{ {
episodes = episodes episodes = episodes
.SkipWhile(i => startItemId.Value.Equals(i.Id)) .SkipWhile(i => !startItemId.Value.Equals(i.Id))
.ToList(); .ToList();
} }

View File

@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] bool? enableRemoteMedia, [FromQuery] bool? enableRemoteMedia,
[FromQuery] bool breakOnNonKeyFrames, [FromQuery] bool breakOnNonKeyFrames = false,
[FromQuery] bool enableRedirection = true) [FromQuery] bool enableRedirection = true)
{ {
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);

View File

@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw)) if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
{ {
return Forbid("Only sha1 password is not allowed."); return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed.");
} }
// Password should always be null // Password should always be null
@@ -267,11 +267,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateUserPassword( public async Task<ActionResult> UpdateUserPassword(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromBody] UpdateUserPassword request) [FromBody, Required] UpdateUserPassword request)
{ {
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{ {
return Forbid("User is not allowed to update the password."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
} }
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
if (success == null) if (success == null)
{ {
return Forbid("Invalid user or password entered."); return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
} }
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
@@ -325,11 +325,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateUserEasyPassword( public ActionResult UpdateUserEasyPassword(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromBody] UpdateUserEasyPassword request) [FromBody, Required] UpdateUserEasyPassword request)
{ {
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{ {
return Forbid("User is not allowed to update the easy password."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
} }
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@@ -367,16 +367,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUser( public async Task<ActionResult> UpdateUser(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromBody] UserDto updateUser) [FromBody, Required] UserDto updateUser)
{ {
if (updateUser == null)
{
return BadRequest();
}
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
{ {
return Forbid("User update not allowed."); return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
} }
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
@@ -407,13 +402,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserPolicy( public async Task<ActionResult> UpdateUserPolicy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromBody] UserPolicy newPolicy) [FromBody, Required] UserPolicy newPolicy)
{ {
if (newPolicy == null)
{
return BadRequest();
}
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
// If removing admin access // If removing admin access
@@ -421,14 +411,14 @@ namespace Jellyfin.Api.Controllers
{ {
if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{ {
return Forbid("There must be at least one user in the system with administrative access."); return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
} }
} }
// If disabling // If disabling
if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
{ {
return Forbid("Administrators cannot be disabled."); return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
} }
// If disabling // If disabling
@@ -436,7 +426,7 @@ namespace Jellyfin.Api.Controllers
{ {
if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
{ {
return Forbid("There must be at least one enabled user in the system."); return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
} }
var currentToken = _authContext.GetAuthorizationInfo(Request).Token; var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
@@ -462,11 +452,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserConfiguration( public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromBody] UserConfiguration userConfig) [FromBody, Required] UserConfiguration userConfig)
{ {
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
{ {
return Forbid("User configuration update not allowed"); return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
} }
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false); await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
@@ -483,7 +473,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("New")] [HttpPost("New")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request) public async Task<ActionResult<UserDto>> CreateUserByName([FromBody, Required] CreateUserByName request)
{ {
var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false); var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
@@ -519,14 +509,14 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Redeems a forgot password pin. /// Redeems a forgot password pin.
/// </summary> /// </summary>
/// <param name="pin">The pin.</param> /// <param name="forgotPasswordPinRequest">The forgot password pin request containing the entered pin.</param>
/// <response code="200">Pin reset process started.</response> /// <response code="200">Pin reset process started.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns> /// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
[HttpPost("ForgotPassword/Pin")] [HttpPost("ForgotPassword/Pin")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string? pin) public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
{ {
var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
return result; return result;
} }

View File

@@ -199,7 +199,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions, [FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,
@@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions, StreamOptions = streamOptions,
MaxHeight = maxHeight, MaxHeight = maxHeight,
MaxWidth = maxWidth, MaxWidth = maxWidth,

View File

@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Merges videos into a single record. /// Merges videos into a single record.
/// </summary> /// </summary>
/// <param name="itemIds">Item id list. This allows multiple, comma delimited.</param> /// <param name="ids">Item id list. This allows multiple, comma delimited.</param>
/// <response code="204">Videos merged.</response> /// <response code="204">Videos merged.</response>
/// <response code="400">Supply at least 2 video ids.</response> /// <response code="400">Supply at least 2 video ids.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
@@ -204,9 +204,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds) public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{ {
var items = itemIds var items = ids
.Select(i => _libraryManager.GetItemById(i)) .Select(i => _libraryManager.GetItemById(i))
.OfType<Video>() .OfType<Video>()
.OrderBy(i => i.Id) .OrderBy(i => i.Id)
@@ -217,9 +217,7 @@ namespace Jellyfin.Api.Controllers
return BadRequest("Please supply at least two videos to merge."); return BadRequest("Please supply at least two videos to merge.");
} }
var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList(); var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
var primaryVersion = videosWithVersions.FirstOrDefault();
if (primaryVersion == null) if (primaryVersion == null)
{ {
primaryVersion = items primaryVersion = items
@@ -364,7 +362,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -379,7 +377,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
@@ -418,7 +416,7 @@ namespace Jellyfin.Api.Controllers
Height = height, Height = height,
VideoBitRate = videoBitRate, VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex, SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames, MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth, MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true, RequireAvc = requireAvc ?? true,
@@ -433,7 +431,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions StreamOptions = streamOptions
}; };
@@ -620,7 +618,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height, [FromQuery] int? height,
[FromQuery] int? videoBitRate, [FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod, [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames, [FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth, [FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc, [FromQuery] bool? requireAvc,
@@ -635,7 +633,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions) [FromQuery] Dictionary<string, string> streamOptions)
{ {
return GetVideoStream( return GetVideoStream(

View File

@@ -7,6 +7,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@@ -70,13 +71,13 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetYears( public ActionResult<QueryResult<BaseItemDto>> GetYears(
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? sortOrder, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,

View File

@@ -427,7 +427,7 @@ namespace Jellyfin.Api.Helpers
if (framerate.HasValue) if (framerate.HasValue)
{ {
builder.Append(",FRAME-RATE=") builder.Append(",FRAME-RATE=")
.Append(framerate.Value); .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
} }
} }

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