Compare commits

...

94 Commits

Author SHA1 Message Date
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
184 changed files with 4367 additions and 1703 deletions

View File

@@ -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

@@ -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

@@ -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

@@ -141,6 +141,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

@@ -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();
@@ -290,12 +312,15 @@ 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);
// 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

@@ -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

@@ -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;
@@ -119,7 +120,9 @@ namespace Emby.Server.Implementations
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 +184,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>
@@ -284,16 +277,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 +386,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 +430,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 +459,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 +491,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 +541,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 +566,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 +757,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 +811,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 +850,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 +862,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 +914,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 +938,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 +951,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 +1031,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
@@ -1369,17 +1281,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

@@ -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.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.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

@@ -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,16 @@ 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
};
} }
private static void RunProcess(string path, string args, string workingDirectory) private static void RunProcess(string path, string args, string workingDirectory)

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;
@@ -1955,9 +1954,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 +1990,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>

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

@@ -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

@@ -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

@@ -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,690 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Plugins;
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;
/// <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);
// This force loads all reference dll's that the plugin uses in the try..catch block.
// Removing this will cause JF to bomb out if referenced dll's cause issues.
assembly.GetExportedTypes();
}
catch (FileLoadException ex)
{
_logger.LogError(ex, "Failed to load assembly {Path}. 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);
}
/// <summary>
/// Saves the manifest back to disk.
/// </summary>
/// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
/// <param name="path">The path where to save the manifest.</param>
/// <returns>True if successful.</returns>
public bool SaveManifest(PluginManifest manifest, string path)
{
if (manifest == null)
{
return false;
}
try
{
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8);
return 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 save plugin manifest. {Path}", path);
return false;
}
}
/// <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, if 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)

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,58 @@ 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);
// Update the manifests, if anything changes.
if (plugin != null)
{
if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
{
plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
}
}
// Remove versions with a target abi that is 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 +234,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 +259,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 +273,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 +289,12 @@ 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
}; };
} }
} }
@@ -282,20 +306,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 +382,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 +414,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 +443,155 @@ 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);
_pluginManager.ImportPluginFrom(targetDir);
}
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
// Set last update time if we were installed before
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));
if (plugin != null)
{
plugin.Manifest.Timestamp = DateTime.UtcNow;
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
}
// Do the install
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
// Do plugin-specific processing
_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,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

@@ -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

@@ -98,7 +98,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);
@@ -144,7 +144,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 +190,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 +229,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);
@@ -325,9 +325,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();
@@ -358,9 +360,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();

View File

@@ -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;
} }
@@ -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,

View File

@@ -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();
} }

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

@@ -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,253 @@ 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 (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
|| 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

@@ -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

@@ -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

@@ -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);

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)

View File

@@ -16,7 +16,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />

View File

@@ -69,7 +69,7 @@ namespace Jellyfin.Api.ModelBinders
} }
catch (FormatException e) catch (FormatException e)
{ {
_logger.LogWarning(e, "Error converting value."); _logger.LogDebug(e, "Error converting value.");
} }
} }

View File

@@ -37,7 +37,7 @@ namespace Jellyfin.Api.ModelBinders
} }
catch (FormatException e) catch (FormatException e)
{ {
_logger.LogWarning(e, "Error converting value."); _logger.LogDebug(e, "Error converting value.");
} }
} }

View File

@@ -69,7 +69,7 @@ namespace Jellyfin.Api.ModelBinders
} }
catch (FormatException e) catch (FormatException e)
{ {
_logger.LogWarning(e, "Error converting value."); _logger.LogDebug(e, "Error converting value.");
} }
} }

View File

@@ -1,4 +1,5 @@
using MediaBrowser.Common.Plugins; using System;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
@@ -22,8 +23,7 @@ namespace Jellyfin.Api.Models
if (page.Plugin != null) if (page.Plugin != null)
{ {
DisplayName = page.Plugin.Name; DisplayName = page.Plugin.Name;
// Don't use "N" because it needs to match Plugin.Id PluginId = page.Plugin.Id;
PluginId = page.Plugin.Id.ToString();
} }
} }
@@ -32,16 +32,14 @@ namespace Jellyfin.Api.Models
/// </summary> /// </summary>
/// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param> /// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
/// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param> /// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param>
public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page) public ConfigurationPageInfo(IPlugin? plugin, PluginPageInfo page)
{ {
Name = page.Name; Name = page.Name;
EnableInMainMenu = page.EnableInMainMenu; EnableInMainMenu = page.EnableInMainMenu;
MenuSection = page.MenuSection; MenuSection = page.MenuSection;
MenuIcon = page.MenuIcon; MenuIcon = page.MenuIcon;
DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName; DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin?.Name : page.DisplayName;
PluginId = plugin?.Id;
// Don't use "N" because it needs to match Plugin.Id
PluginId = plugin.Id.ToString();
} }
/// <summary> /// <summary>
@@ -80,6 +78,6 @@ namespace Jellyfin.Api.Models
/// Gets or sets the plugin id. /// Gets or sets the plugin id.
/// </summary> /// </summary>
/// <value>The plugin id.</value> /// <value>The plugin id.</value>
public string? PluginId { get; set; } public Guid? PluginId { get; set; }
} }
} }

View File

@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Api.Models.LiveTvDtos
{
/// <summary>
/// Set channel mapping dto.
/// </summary>
public class SetChannelMappingDto
{
/// <summary>
/// Gets or sets the provider id.
/// </summary>
[Required]
public string ProviderId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the tuner channel id.
/// </summary>
[Required]
public string TunerChannelId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the provider channel id.
/// </summary>
[Required]
public string ProviderChannelId { get; set; } = string.Empty;
}
}

View File

@@ -10,6 +10,61 @@ namespace Jellyfin.Api.Models.MediaInfoDtos
/// </summary> /// </summary>
public class OpenLiveStreamDto public class OpenLiveStreamDto
{ {
/// <summary>
/// Gets or sets the open token.
/// </summary>
public string? OpenToken { get; set; }
/// <summary>
/// Gets or sets the user id.
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// Gets or sets the play session id.
/// </summary>
public string? PlaySessionId { get; set; }
/// <summary>
/// Gets or sets the max streaming bitrate.
/// </summary>
public int? MaxStreamingBitrate { get; set; }
/// <summary>
/// Gets or sets the start time in ticks.
/// </summary>
public long? StartTimeTicks { get; set; }
/// <summary>
/// Gets or sets the audio stream index.
/// </summary>
public int? AudioStreamIndex { get; set; }
/// <summary>
/// Gets or sets the subtitle stream index.
/// </summary>
public int? SubtitleStreamIndex { get; set; }
/// <summary>
/// Gets or sets the max audio channels.
/// </summary>
public int? MaxAudioChannels { get; set; }
/// <summary>
/// Gets or sets the item id.
/// </summary>
public Guid? ItemId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable direct play.
/// </summary>
public bool? EnableDirectPlay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enale direct stream.
/// </summary>
public bool? EnableDirectStream { get; set; }
/// <summary> /// <summary>
/// Gets or sets the device profile. /// Gets or sets the device profile.
/// </summary> /// </summary>

View File

@@ -24,7 +24,7 @@ namespace Jellyfin.Api.Models.PlaylistDtos
/// <summary> /// <summary>
/// Gets or sets the user id. /// Gets or sets the user id.
/// </summary> /// </summary>
public Guid UserId { get; set; } public Guid? UserId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the media type. /// Gets or sets the media type.

View File

@@ -23,7 +23,6 @@ namespace Jellyfin.Data.Entities
Client = client; Client = client;
SortBy = "SortName"; SortBy = "SortName";
ViewType = ViewType.Poster;
SortOrder = SortOrder.Ascending; SortOrder = SortOrder.Ascending;
RememberSorting = false; RememberSorting = false;
RememberIndexing = false; RememberIndexing = false;

View File

@@ -2,9 +2,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@@ -71,7 +71,7 @@ namespace Jellyfin.Data.Entities
EnableAutoLogin = false; EnableAutoLogin = false;
PlayDefaultAudioTrack = true; PlayDefaultAudioTrack = true;
SubtitleMode = SubtitlePlaybackMode.Default; SubtitleMode = SubtitlePlaybackMode.Default;
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups; SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
AddDefaultPermissions(); AddDefaultPermissions();
AddDefaultPreferences(); AddDefaultPreferences();
@@ -326,7 +326,7 @@ namespace Jellyfin.Data.Entities
/// <summary> /// <summary>
/// Gets or sets the level of sync play permissions this user has. /// Gets or sets the level of sync play permissions this user has.
/// </summary> /// </summary>
public SyncPlayAccess SyncPlayAccess { get; set; } public SyncPlayUserAccessType SyncPlayAccess { get; set; }
/// <summary> /// <summary>
/// Gets or sets the row version. /// Gets or sets the row version.
@@ -413,6 +413,44 @@ namespace Jellyfin.Data.Entities
return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter); return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
} }
/// <summary>
/// Gets the user's preferences for the given preference kind.
/// </summary>
/// <param name="preference">The preference kind.</param>
/// <typeparam name="T">Type of preference.</typeparam>
/// <returns>A {T} array containing the user's preference.</returns>
public T[] GetPreferenceValues<T>(PreferenceKind preference)
{
var val = Preferences.First(p => p.Kind == preference).Value;
if (string.IsNullOrEmpty(val))
{
return Array.Empty<T>();
}
// Convert array of {string} to array of {T}
var converter = TypeDescriptor.GetConverter(typeof(T));
var stringValues = val.Split(Delimiter);
var convertedCount = 0;
var parsedValues = new T[stringValues.Length];
for (var i = 0; i < stringValues.Length; i++)
{
try
{
var parsedValue = converter.ConvertFromString(stringValues[i].Trim());
if (parsedValue != null)
{
parsedValues[convertedCount++] = (T)parsedValue;
}
}
catch (FormatException)
{
// Unable to convert value
}
}
return parsedValues[..convertedCount];
}
/// <summary> /// <summary>
/// Sets the specified preference to the given value. /// Sets the specified preference to the given value.
/// </summary> /// </summary>
@@ -421,7 +459,19 @@ namespace Jellyfin.Data.Entities
public void SetPreference(PreferenceKind preference, string[] values) public void SetPreference(PreferenceKind preference, string[] values)
{ {
Preferences.First(p => p.Kind == preference).Value Preferences.First(p => p.Kind == preference).Value
= string.Join(Delimiter.ToString(CultureInfo.InvariantCulture), values); = string.Join(Delimiter, values);
}
/// <summary>
/// Sets the specified preference to the given value.
/// </summary>
/// <param name="preference">The preference kind.</param>
/// <param name="values">The values.</param>
/// <typeparam name="T">The type of value.</typeparam>
public void SetPreference<T>(PreferenceKind preference, T[] values)
{
Preferences.First(p => p.Kind == preference).Value
= string.Join(Delimiter, values);
} }
/// <summary> /// <summary>
@@ -441,7 +491,7 @@ namespace Jellyfin.Data.Entities
/// <returns><c>True</c> if the folder is in the user's grouped folders.</returns> /// <returns><c>True</c> if the folder is in the user's grouped folders.</returns>
public bool IsFolderGrouped(Guid id) public bool IsFolderGrouped(Guid id)
{ {
return GetPreference(PreferenceKind.GroupedFolders).Any(i => new Guid(i) == id); return Array.IndexOf(GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), id) != -1;
} }
private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)

View File

@@ -0,0 +1,28 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// Enum SyncPlayAccessRequirementType.
/// </summary>
public enum SyncPlayAccessRequirementType
{
/// <summary>
/// User must have access to SyncPlay, in some form.
/// </summary>
HasAccess = 0,
/// <summary>
/// User must be able to create groups.
/// </summary>
CreateGroup = 1,
/// <summary>
/// User must be able to join groups.
/// </summary>
JoinGroup = 2,
/// <summary>
/// User must be in a group.
/// </summary>
IsInGroup = 3
}
}

View File

@@ -1,9 +1,9 @@
namespace Jellyfin.Data.Enums namespace Jellyfin.Data.Enums
{ {
/// <summary> /// <summary>
/// Enum SyncPlayAccess. /// Enum SyncPlayUserAccessType.
/// </summary> /// </summary>
public enum SyncPlayAccess public enum SyncPlayUserAccessType
{ {
/// <summary> /// <summary>
/// User can create groups and join them. /// User can create groups and join them.

View File

@@ -1,4 +1,4 @@
namespace Jellyfin.Data.Enums namespace Jellyfin.Data.Enums
{ {
/// <summary> /// <summary>
/// An enum representing the type of view for a library or collection. /// An enum representing the type of view for a library or collection.
@@ -6,33 +6,108 @@
public enum ViewType public enum ViewType
{ {
/// <summary> /// <summary>
/// Shows banners. /// Shows albums.
/// </summary> /// </summary>
Banner = 0, Albums = 0,
/// <summary> /// <summary>
/// Shows a list of content. /// Shows album artists.
/// </summary> /// </summary>
List = 1, AlbumArtists = 1,
/// <summary> /// <summary>
/// Shows poster artwork. /// Shows artists.
/// </summary> /// </summary>
Poster = 2, Artists = 2,
/// <summary> /// <summary>
/// Shows poster artwork with a card containing the name and year. /// Shows channels.
/// </summary> /// </summary>
PosterCard = 3, Channels = 3,
/// <summary> /// <summary>
/// Shows a thumbnail. /// Shows collections.
/// </summary> /// </summary>
Thumb = 4, Collections = 4,
/// <summary> /// <summary>
/// Shows a thumbnail with a card containing the name and year. /// Shows episodes.
/// </summary> /// </summary>
ThumbCard = 5 Episodes = 5,
/// <summary>
/// Shows favorites.
/// </summary>
Favorites = 6,
/// <summary>
/// Shows genres.
/// </summary>
Genres = 7,
/// <summary>
/// Shows guide.
/// </summary>
Guide = 8,
/// <summary>
/// Shows movies.
/// </summary>
Movies = 9,
/// <summary>
/// Shows networks.
/// </summary>
Networks = 10,
/// <summary>
/// Shows playlists.
/// </summary>
Playlists = 11,
/// <summary>
/// Shows programs.
/// </summary>
Programs = 12,
/// <summary>
/// Shows recordings.
/// </summary>
Recordings = 13,
/// <summary>
/// Shows schedule.
/// </summary>
Schedule = 14,
/// <summary>
/// Shows series.
/// </summary>
Series = 15,
/// <summary>
/// Shows shows.
/// </summary>
Shows = 16,
/// <summary>
/// Shows songs.
/// </summary>
Songs = 17,
/// <summary>
/// Shows songs.
/// </summary>
Suggestions = 18,
/// <summary>
/// Shows trailers.
/// </summary>
Trailers = 19,
/// <summary>
/// Shows upcoming.
/// </summary>
Upcoming = 20
} }
} }

View File

@@ -41,8 +41,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -435,7 +435,7 @@ namespace Jellyfin.Drawing.Skia
0f, 0f,
kernelOffset, kernelOffset,
SKShaderTileMode.Clamp, SKShaderTileMode.Clamp,
false); true);
canvas.DrawBitmap( canvas.DrawBitmap(
source, source,

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
using SkiaSharp; using SkiaSharp;
namespace Jellyfin.Drawing.Skia namespace Jellyfin.Drawing.Skia
@@ -118,6 +119,16 @@ namespace Jellyfin.Drawing.Skia
}; };
canvas.DrawRect(0, 0, width, height, paintColor); canvas.DrawRect(0, 0, width, height, paintColor);
var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
// use the system fallback to find a typeface for the given CJK character
var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
if (!string.IsNullOrEmpty(filteredName))
{
typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
}
// draw library name // draw library name
var textPaint = new SKPaint var textPaint = new SKPaint
{ {
@@ -125,7 +136,7 @@ namespace Jellyfin.Drawing.Skia
Style = SKPaintStyle.Fill, Style = SKPaintStyle.Fill,
TextSize = 112, TextSize = 112,
TextAlign = SKTextAlign.Center, TextAlign = SKTextAlign.Center,
Typeface = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright), Typeface = typeFace,
IsAntialias = true IsAntialias = true
}; };

View File

@@ -27,6 +27,16 @@ namespace Jellyfin.Networking.Configuration
/// </summary> /// </summary>
public bool RequireHttps { get; set; } public bool RequireHttps { get; set; }
/// <summary>
/// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
/// </summary>
public string CertificatePath { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
/// </summary>
public string CertificatePassword { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
/// </summary> /// </summary>
@@ -83,7 +93,7 @@ namespace Jellyfin.Networking.Configuration
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// In order for HTTPS to be used, in addition to setting this to true, valid values must also be /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
/// provided for <see cref="ServerConfiguration.CertificatePath"/> and <see cref="ServerConfiguration.CertificatePassword"/>. /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
/// </remarks> /// </remarks>
public bool EnableHttps { get; set; } public bool EnableHttps { get; set; }
@@ -214,7 +224,7 @@ namespace Jellyfin.Networking.Configuration
public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>(); public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
/// <summary> /// <summary>
/// Gets or sets the known proxies. /// Gets or sets the known proxies. If the proxy is a network, it's added to the KnownNetworks.
/// </summary> /// </summary>
public string[] KnownProxies { get; set; } = Array.Empty<string>(); public string[] KnownProxies { get; set; } = Array.Empty<string>();
} }

View File

@@ -387,7 +387,7 @@ namespace Jellyfin.Networking.Manager
// Get the first LAN interface address that isn't a loopback. // Get the first LAN interface address that isn't a loopback.
var interfaces = CreateCollection(_interfaceAddresses var interfaces = CreateCollection(_interfaceAddresses
.Exclude(_bindExclusions) .Exclude(_bindExclusions)
.Where(p => IsInLocalNetwork(p)) .Where(IsInLocalNetwork)
.OrderBy(p => p.Tag)); .OrderBy(p => p.Tag));
if (interfaces.Count > 0) if (interfaces.Count > 0)
@@ -591,7 +591,7 @@ namespace Jellyfin.Networking.Manager
else // Used in testing only. else // Used in testing only.
{ {
// Format is <IPAddress>,<Index>,<Name>: <next interface>. Set index to -ve to simulate a gateway. // Format is <IPAddress>,<Index>,<Name>: <next interface>. Set index to -ve to simulate a gateway.
var interfaceList = MockNetworkSettings.Split(':'); var interfaceList = MockNetworkSettings.Split('|');
foreach (var details in interfaceList) foreach (var details in interfaceList)
{ {
var parts = details.Split(','); var parts = details.Split(',');
@@ -784,7 +784,7 @@ namespace Jellyfin.Networking.Manager
} }
else else
{ {
_logger.LogDebug("Invalid or unknown network {Token}.", token); _logger.LogDebug("Invalid or unknown object {Token}.", token);
} }
} }
@@ -913,15 +913,6 @@ namespace Jellyfin.Networking.Manager
{ {
string[] lanAddresses = config.LocalNetworkAddresses; string[] lanAddresses = config.LocalNetworkAddresses;
// TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334
if (lanAddresses.Length == 1 && lanAddresses[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1)
{
lanAddresses = lanAddresses[0].Split(',');
}
// TODO: end fix: https://github.com/jellyfin/jellyfin-web/issues/1334
// Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
if (config.IgnoreVirtualInterfaces) if (config.IgnoreVirtualInterfaces)
{ {
@@ -1314,9 +1305,7 @@ namespace Jellyfin.Networking.Manager
return true; return true;
} }
// Have to return something, so return an internal address _logger.LogDebug("{Source}: External request received, but no WAN interface found. Need to route through internal network.", source);
_logger.LogWarning("{Source}: External request received, however, no WAN interface found.", source);
return false; return false;
} }
} }

View File

@@ -25,11 +25,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Linq.Async" Version="5.0.0" /> <PackageReference Include="System.Linq.Async" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@@ -376,14 +376,14 @@ namespace Jellyfin.Server.Implementations.Users
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
AccessSchedules = user.AccessSchedules.ToArray(), AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels)?.Select(Guid.Parse).ToArray(), EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders)?.Select(Guid.Parse).ToArray(), EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders), EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
SyncPlayAccess = user.SyncPlayAccess, SyncPlayAccess = user.SyncPlayAccess,
BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels)?.Select(Guid.Parse).ToArray(), BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders)?.Select(Guid.Parse).ToArray(), BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse<UnratedItem>).ToArray() BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
} }
}; };
} }
@@ -704,13 +704,11 @@ namespace Jellyfin.Server.Implementations.Users
} }
// TODO: fix this at some point // TODO: fix this at some point
user.SetPreference( user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
PreferenceKind.BlockUnratedItems,
policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty<string>());
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user); dbContext.Update(user);

View File

@@ -107,5 +107,28 @@ namespace Jellyfin.Server.Extensions
{ {
return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>(); return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>();
} }
/// <summary>
/// Adds robots.txt redirection to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseRobotsRedirection(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>();
}
/// <summary>
/// Adds /emby and /mediabrowser route trimming to the application pipeline.
/// </summary>
/// <remarks>
/// This must be injected before any path related middleware.
/// </remarks>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UsePathTrim(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<LegacyEmbyRouteRewriteMiddleware>();
}
} }
} }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets;
using System.Reflection; using System.Reflection;
using Emby.Server.Implementations; using Emby.Server.Implementations;
using Jellyfin.Api.Auth; using Jellyfin.Api.Auth;
@@ -20,10 +21,12 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers; using Jellyfin.Api.Controllers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Configuration; using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters; using Jellyfin.Server.Filters;
using Jellyfin.Server.Formatters; using Jellyfin.Server.Formatters;
using MediaBrowser.Common.Json; using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -127,18 +130,32 @@ namespace Jellyfin.Server.Extensions
policy.AddRequirements(new RequiresElevationRequirement()); policy.AddRequirements(new RequiresElevationRequirement());
}); });
options.AddPolicy( options.AddPolicy(
Policies.SyncPlayAccess, Policies.SyncPlayHasAccess,
policy => policy =>
{ {
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.JoinGroups)); policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
}); });
options.AddPolicy( options.AddPolicy(
Policies.SyncPlayCreateGroupAccess, Policies.SyncPlayCreateGroup,
policy => policy =>
{ {
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.CreateAndJoinGroups)); policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
});
options.AddPolicy(
Policies.SyncPlayJoinGroup,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
});
options.AddPolicy(
Policies.SyncPlayIsInGroup,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
}); });
}); });
} }
@@ -159,22 +176,33 @@ namespace Jellyfin.Server.Extensions
/// </summary> /// </summary>
/// <param name="serviceCollection">The service collection.</param> /// <param name="serviceCollection">The service collection.</param>
/// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param> /// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param>
/// <param name="knownProxies">A list of all known proxies to trust for X-Forwarded-For.</param> /// <param name="config">The <see cref="NetworkConfiguration"/>.</param>
/// <returns>The MVC builder.</returns> /// <returns>The MVC builder.</returns>
public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies, IReadOnlyList<string> knownProxies) public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies, NetworkConfiguration config)
{ {
IMvcBuilder mvcBuilder = serviceCollection IMvcBuilder mvcBuilder = serviceCollection
.AddCors() .AddCors()
.AddTransient<ICorsPolicyProvider, CorsPolicyProvider>() .AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
.Configure<ForwardedHeadersOptions>(options => .Configure<ForwardedHeadersOptions>(options =>
{ {
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
for (var i = 0; i < knownProxies.Count; i++) if (config.KnownProxies.Length == 0)
{ {
if (IPAddress.TryParse(knownProxies[i], out var address)) options.KnownNetworks.Clear();
{ options.KnownProxies.Clear();
options.KnownProxies.Add(address); }
} else
{
AddProxyAddresses(config, config.KnownProxies, options);
}
// Only set forward limit if we have some known proxies or some known networks.
if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
{
options.ForwardLimit = null;
} }
}) })
.AddMvc(opts => .AddMvc(opts =>
@@ -204,6 +232,7 @@ namespace Jellyfin.Server.Extensions
options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented; options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition; options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling; options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
options.JsonSerializerOptions.PropertyNameCaseInsensitive = jsonOptions.PropertyNameCaseInsensitive;
options.JsonSerializerOptions.Converters.Clear(); options.JsonSerializerOptions.Converters.Clear();
foreach (var converter in jsonOptions.Converters) foreach (var converter in jsonOptions.Converters)
@@ -284,10 +313,60 @@ namespace Jellyfin.Server.Extensions
c.OperationFilter<SecurityRequirementsOperationFilter>(); c.OperationFilter<SecurityRequirementsOperationFilter>();
c.OperationFilter<FileResponseFilter>(); c.OperationFilter<FileResponseFilter>();
c.OperationFilter<ParameterObsoleteFilter>();
c.DocumentFilter<WebsocketModelFilter>(); c.DocumentFilter<WebsocketModelFilter>();
}); });
} }
/// <summary>
/// Sets up the proxy configuration based on the addresses in <paramref name="allowedProxies"/>.
/// </summary>
/// <param name="config">The <see cref="NetworkConfiguration"/> containing the config settings.</param>
/// <param name="allowedProxies">The string array to parse.</param>
/// <param name="options">The <see cref="ForwardedHeadersOptions"/> instance.</param>
internal static void AddProxyAddresses(NetworkConfiguration config, string[] allowedProxies, ForwardedHeadersOptions options)
{
for (var i = 0; i < allowedProxies.Length; i++)
{
if (IPNetAddress.TryParse(allowedProxies[i], out var addr))
{
AddIpAddress(config, options, addr.Address, addr.PrefixLength);
}
else if (IPHost.TryParse(allowedProxies[i], out var host))
{
foreach (var address in host.GetAddresses())
{
AddIpAddress(config, options, addr.Address, addr.PrefixLength);
}
}
}
}
private static void AddIpAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength)
{
if ((!config.EnableIPV4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPV6 && addr.AddressFamily == AddressFamily.InterNetworkV6))
{
return;
}
// In order for dual-mode sockets to be used, IP6 has to be enabled in JF and an interface has to have an IP6 address.
if (addr.AddressFamily == AddressFamily.InterNetwork && config.EnableIPV6)
{
// If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format.
// https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0 .
addr = addr.MapToIPv6();
}
if (prefixLength == 32)
{
options.KnownProxies.Add(addr);
}
else
{
options.KnownNetworks.Add(new IPNetwork(addr, prefixLength));
}
}
private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
{ {
/* /*

View File

@@ -14,7 +14,8 @@ namespace Jellyfin.Server.Filters
{ {
Schema = new OpenApiSchema Schema = new OpenApiSchema
{ {
Type = "file" Type = "string",
Format = "binary"
} }
}; };

View File

@@ -0,0 +1,37 @@
using System;
using System.Linq;
using Jellyfin.Api.Attributes;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
{
/// <summary>
/// Mark parameter as deprecated if it has the <see cref="ParameterObsoleteAttribute"/>.
/// </summary>
public class ParameterObsoleteFilter : IOperationFilter
{
/// <inheritdoc />
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
foreach (var parameterDescription in context.ApiDescription.ParameterDescriptions)
{
if (parameterDescription
.CustomAttributes()
.OfType<ParameterObsoleteAttribute>()
.Any())
{
foreach (var parameter in operation.Parameters)
{
if (parameter.Name.Equals(parameterDescription.Name, StringComparison.Ordinal))
{
parameter.Deprecated = true;
break;
}
}
}
}
}
}
}

View File

@@ -40,8 +40,8 @@
<PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.2" />
<PackageReference Include="prometheus-net" Version="4.0.0" /> <PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />

View File

@@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Middleware
{
/// <summary>
/// Removes /emby and /mediabrowser from requested route.
/// </summary>
public class LegacyEmbyRouteRewriteMiddleware
{
private const string EmbyPath = "/emby";
private const string MediabrowserPath = "/mediabrowser";
private readonly RequestDelegate _next;
private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
/// </summary>
/// <param name="next">The next delegate in the pipeline.</param>
/// <param name="logger">The logger.</param>
public LegacyEmbyRouteRewriteMiddleware(
RequestDelegate next,
ILogger<LegacyEmbyRouteRewriteMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Executes the middleware action.
/// </summary>
/// <param name="httpContext">The current HTTP context.</param>
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
var localPath = httpContext.Request.Path.ToString();
if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase))
{
httpContext.Request.Path = localPath[EmbyPath.Length..];
_logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath);
}
else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase))
{
httpContext.Request.Path = localPath[MediabrowserPath.Length..];
_logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath);
}
await _next(httpContext).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Middleware
{
/// <summary>
/// Redirect requests to robots.txt to web/robots.txt.
/// </summary>
public class RobotsRedirectionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RobotsRedirectionMiddleware> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class.
/// </summary>
/// <param name="next">The next delegate in the pipeline.</param>
/// <param name="logger">The logger.</param>
public RobotsRedirectionMiddleware(
RequestDelegate next,
ILogger<RobotsRedirectionMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Executes the middleware action.
/// </summary>
/// <param name="httpContext">The current HTTP context.</param>
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
var localPath = httpContext.Request.Path.ToString();
if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
httpContext.Response.Redirect("web/robots.txt");
return;
}
await _next(httpContext).ConfigureAwait(false);
}
}
}

View File

@@ -8,7 +8,6 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@@ -81,6 +80,8 @@ namespace Jellyfin.Server.Migrations.Routines
{ "unstable", ChromecastVersion.Unstable } { "unstable", ChromecastVersion.Unstable }
}; };
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null)) using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
{ {
@@ -97,6 +98,15 @@ namespace Jellyfin.Server.Migrations.Routines
var itemId = new Guid(result[1].ToBlob()); var itemId = new Guid(result[1].ToBlob());
var dtoUserId = new Guid(result[1].ToBlob()); var dtoUserId = new Guid(result[1].ToBlob());
var client = result[2].ToString();
var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}";
if (displayPrefs.Contains(displayPreferencesKey))
{
// Duplicate display preference.
continue;
}
displayPrefs.Add(displayPreferencesKey);
var existingUser = _userManager.GetUserById(dtoUserId); var existingUser = _userManager.GetUserById(dtoUserId);
if (existingUser == null) if (existingUser == null)
{ {
@@ -109,7 +119,7 @@ namespace Jellyfin.Server.Migrations.Routines
: ChromecastVersion.Stable; : ChromecastVersion.Stable;
dto.CustomPrefs.Remove("chromecastVersion"); dto.CustomPrefs.Remove("chromecastVersion");
var displayPreferences = new DisplayPreferences(dtoUserId, itemId, result[2].ToString()) var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client)
{ {
IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null, IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
ShowBackdrop = dto.ShowBackdrop, ShowBackdrop = dto.ShowBackdrop,
@@ -185,7 +195,13 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var (key, value) in dto.CustomPrefs) foreach (var (key, value) in dto.CustomPrefs)
{ {
dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value)); // Custom display preferences can have a key collision.
var indexKey = $"{displayPreferences.UserId}|{itemId}|{displayPreferences.Client}|{key}";
if (!customDisplayPrefs.Contains(indexKey))
{
dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value));
customDisplayPrefs.Add(indexKey);
}
} }
dbContext.Add(displayPreferences); dbContext.Add(displayPreferences);

View File

@@ -168,9 +168,9 @@ namespace Jellyfin.Server.Migrations.Routines
} }
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);

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
@@ -19,3 +20,5 @@ using System.Runtime.InteropServices;
// 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
// COM, set the ComVisible attribute to true on that type. // COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")]

View File

@@ -52,7 +52,7 @@ namespace Jellyfin.Server
{ {
options.HttpsPort = _serverApplicationHost.HttpsPort; options.HttpsPort = _serverApplicationHost.HttpsPort;
}); });
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration().KnownProxies); services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
services.AddJellyfinApiSwagger(); services.AddJellyfinApiSwagger();
@@ -128,6 +128,8 @@ namespace Jellyfin.Server
mainApp.UseHttpsRedirection(); mainApp.UseHttpsRedirection();
} }
// This must be injected before any path related middleware.
mainApp.UsePathTrim();
mainApp.UseStaticFiles(); mainApp.UseStaticFiles();
if (appConfig.HostWebClient()) if (appConfig.HostWebClient())
{ {
@@ -142,6 +144,8 @@ namespace Jellyfin.Server
RequestPath = "/web", RequestPath = "/web",
ContentTypeProvider = extensionProvider ContentTypeProvider = extensionProvider
}); });
mainApp.UseRobotsRedirection();
} }
mainApp.UseAuthentication(); mainApp.UseAuthentication();

View File

@@ -2,11 +2,16 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common namespace MediaBrowser.Common
{ {
/// <summary>
/// Delegate used with GetExports{T}.
/// </summary>
/// <param name="type">Type to create.</param>
/// <returns>New instance of type <param>type</param>.</returns>
public delegate object CreationDelegate(Type type);
/// <summary> /// <summary>
/// An interface to be implemented by the applications hosting a kernel. /// An interface to be implemented by the applications hosting a kernel.
/// </summary> /// </summary>
@@ -53,6 +58,11 @@ namespace MediaBrowser.Common
/// <value>The application version.</value> /// <value>The application version.</value>
Version ApplicationVersion { get; } Version ApplicationVersion { get; }
/// <summary>
/// Gets or sets the service provider.
/// </summary>
IServiceProvider ServiceProvider { get; set; }
/// <summary> /// <summary>
/// Gets the application version. /// Gets the application version.
/// </summary> /// </summary>
@@ -71,12 +81,6 @@ namespace MediaBrowser.Common
/// </summary> /// </summary>
string ApplicationUserAgentAddress { get; } string ApplicationUserAgentAddress { get; }
/// <summary>
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
IReadOnlyList<IPlugin> Plugins { get; }
/// <summary> /// <summary>
/// Gets all plugin assemblies which implement a custom rest api. /// Gets all plugin assemblies which implement a custom rest api.
/// </summary> /// </summary>
@@ -101,6 +105,22 @@ namespace MediaBrowser.Common
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns> /// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true); IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true);
/// <summary>
/// Gets the exports.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="defaultFunc">Delegate function that gets called to create the object.</param>
/// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param>
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true);
/// <summary>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
IEnumerable<Type> GetExportTypes<T>();
/// <summary> /// <summary>
/// Resolves this instance. /// Resolves this instance.
/// </summary> /// </summary>
@@ -114,12 +134,6 @@ namespace MediaBrowser.Common
/// <returns>A task.</returns> /// <returns>A task.</returns>
Task Shutdown(); Task Shutdown();
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
void RemovePlugin(IPlugin plugin);
/// <summary> /// <summary>
/// Initializes this instance. /// Initializes this instance.
/// </summary> /// </summary>

View File

@@ -0,0 +1,30 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Converts a number to a boolean.
/// This is needed for HDHomerun.
/// </summary>
public class JsonBoolNumberConverter : JsonConverter<bool>
{
/// <inheritdoc />
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
return Convert.ToBoolean(reader.GetInt32());
}
return reader.GetBoolean();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
}

View File

@@ -45,7 +45,7 @@ namespace MediaBrowser.Common.Json.Converters
{ {
// TODO log when upgraded to .Net6 // TODO log when upgraded to .Net6
// https://github.com/dotnet/runtime/issues/42975 // https://github.com/dotnet/runtime/issues/42975
// _logger.LogWarning(e, "Error converting value."); // _logger.LogDebug(e, "Error converting value.");
} }
} }

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