Compare commits

..

170 Commits

Author SHA1 Message Date
Bond-009
56b05f4e2b Merge pull request #6175 from nielsvanvelzen/crobibero-is-stupid
Fix routeMediaSourceId route parameter in SubtitleController GetSubtitle

(cherry picked from commit 04daf0ff49)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-06-13 19:30:34 -04:00
Joshua M. Boniface
e8148ec0bd Merge pull request #6131 from BaronGreenback/Fix_NetworkFlooding
Fix network flooding issue

(cherry picked from commit b060d9d0f1)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-06-02 22:08:16 -04:00
Claus Vium
77c5c53598 Merge pull request #6043 from peterspenler/feature/chromecast-aac-handling
Reorder requested audio channels checks

(cherry picked from commit ffe2770388)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-27 02:46:02 -04:00
Claus Vium
d2db73b876 Merge pull request #6038 from crobibero/delete-existing-sessions
Don't logout if deviceId is null

(cherry picked from commit 1594385497)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-27 02:46:02 -04:00
Joshua M. Boniface
49873c3d7f Bump version to 10.7.6 2021-05-20 22:06:25 -04:00
Bond-009
0b577c8a44 Merge pull request #6053 from jellyfin/cuda-fix
Fix the 'No decoder surfaces left' error on Cuda

(cherry picked from commit 9b82b37095)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-20 22:05:00 -04:00
Bond-009
6386606a53 Merge pull request #5987 from Bond-009/ioob
PathExtensions: Fix index out of bounds in TryReplaceSubPath
(cherry picked from commit a6ee4632ce)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-05-20 22:05:00 -04:00
dkanada
cc908210d9 Merge pull request #6022 from jellyfin/revert-remux-perm
Revert remuxing permission changes from #5859
2021-05-11 21:07:36 +09:00
Claus Vium
dec30ade8f Revert remuxing permission changes from #5859
Clients cannot currently handle it properly.
2021-05-10 08:35:23 +02:00
Joshua M. Boniface
55a8d2555e Bump version to 10.7.5 2021-05-04 22:08:44 -04:00
Joshua M. Boniface
b7c3510da1 Revert "Merge pull request #5943 from Maxr1998/device-profile-defaults"
This PR broke direct play in JMP and caused aspect ratio issues in web.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(cherry picked from commit 7227d0d48b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-01-23 15:37:25 -05:00
239 changed files with 4800 additions and 2125 deletions

View File

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

View File

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

View File

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

View File

@@ -160,7 +160,6 @@ jobs:
dependsOn:
- BuildPackage
- BuildDocker
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
@@ -186,13 +185,14 @@ jobs:
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: succeeded('BuildPackage')
pool:
vmImage: 'ubuntu-latest'
variables:
- name: JellyfinVersion
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
steps:
- task: UseDotNet@2
displayName: 'Use .NET 5.0 sdk'
@@ -204,12 +204,19 @@ jobs:
displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: 'pack'
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'
versioningScheme: 'off'
command: 'custom'
projects: |
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
displayName: 'Build Unstable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: 'custom'
projects: |
@@ -232,7 +239,7 @@ jobs:
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
nuGetFeedType: 'external'
publishFeedCredentials: 'NugetOrg'
allowPackageConflicts: true # This ignores an error if the version already exists

View File

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

View File

@@ -6,7 +6,7 @@ variables:
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion
value: 5.0.100
value: 5.0.103
pr:
autoCancel: true
@@ -61,6 +61,3 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-api-client.yml

View File

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

View File

@@ -49,6 +49,7 @@
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
@@ -103,6 +104,7 @@
- [shemanaev](https://github.com/shemanaev)
- [skaro13](https://github.com/skaro13)
- [sl1288](https://github.com/sl1288)
- [Smith00101010](https://github.com/Smith00101010)
- [sorinyo2004](https://github.com/sorinyo2004)
- [sparky8251](https://github.com/sparky8251)
- [spookbits](https://github.com/spookbits)
@@ -141,6 +143,8 @@
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
- [peterspenler](https://github.com/peterspenler)
# Emby Contributors

View File

@@ -96,6 +96,7 @@ namespace Emby.Dlna.Didl
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))
{
// writer.WriteStartDocument();

View File

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

View File

@@ -128,7 +128,8 @@ namespace Emby.Dlna.Main
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
if (_disabled)
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
@@ -228,7 +229,10 @@ namespace Emby.Dlna.Main
{
try
{
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
if (communicationsServer != null)
{
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
}
}
catch (Exception ex)
{
@@ -313,9 +317,12 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
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.PublicPort;
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
{
// DLNA will only work over http, so we must reset to http:// : {port}.
uri.Scheme = "http";
uri.Port = _netConfig.HttpServerPortNumber;
}
var device = new SsdpRootDevice
{

View File

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

View File

@@ -132,7 +132,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{
if (_disposed)
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
return;
}
@@ -826,7 +826,7 @@ namespace Emby.Dlna.PlayTo
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (name == SessionMessageType.PlayState)
if (name == SessionMessageType.Playstate)
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
@@ -896,16 +896,16 @@ namespace Emby.Dlna.PlayTo
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];
if (string.Equals(part, "audio", 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;
}
}
}
@@ -943,11 +943,7 @@ namespace Emby.Dlna.PlayTo
request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
// Be careful, IsDirectStream==true by default (Static != false or not in query).
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -582,7 +582,26 @@ namespace Emby.Server.Implementations.Dto
{
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
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);
}
}
@@ -1138,7 +1157,7 @@ namespace Emby.Server.Implementations.Dto
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, episodeSeries);
}
@@ -1188,7 +1207,7 @@ namespace Emby.Server.Implementations.Dto
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, series);
}

View File

@@ -106,8 +106,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.StartDiscovery();
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
}
private void Stop()
@@ -118,13 +116,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
_timer?.Dispose();
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
}
private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
{
NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
}
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)

View File

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

View File

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

View File

@@ -249,9 +249,18 @@ namespace Emby.Server.Implementations.IO
// Issue #2354 get the size of files behind symbolic links
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
try
{
result.Length = thisFileStream.Length;
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
{
result.Length = thisFileStream.Length;
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
}
@@ -582,9 +591,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
{
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption));
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
}
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
@@ -594,16 +601,16 @@ namespace Emby.Server.Implementations.IO
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
// 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)
{
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)
{
@@ -625,10 +632,10 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
var directoryInfo = new DirectoryInfo(path);
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var enumerationOptions = GetEnumerationOptions(recursive);
return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption))
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption)));
return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
}
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@@ -638,8 +645,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
{
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
return Directory.EnumerateDirectories(path, "*", searchOption);
return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
}
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
@@ -649,16 +655,16 @@ namespace Emby.Server.Implementations.IO
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
// 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)
{
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)
{
@@ -679,8 +685,18 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
}
private EnumerationOptions GetEnumerationOptions(bool recursive)
{
return new EnumerationOptions
{
RecurseSubdirectories = recursive,
IgnoreInaccessible = true,
// Don't skip any files.
AttributesToSkip = 0
};
}
private static void RunProcess(string path, string args, string workingDirectory)

View File

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

View File

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

View File

@@ -203,7 +203,7 @@ namespace Emby.Server.Implementations.Library
}
}
return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList();
return SortMediaSources(list);
}
public MediaProtocol GetPathProtocol(string path)
@@ -437,7 +437,7 @@ namespace Emby.Server.Implementations.Library
}
}
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
{
@@ -452,8 +452,9 @@ namespace Emby.Server.Implementations.Library
{
var stream = i.VideoStream;
return stream == null || stream.Width == null ? 0 : stream.Width.Value;
return stream?.Width ?? 0;
})
.Where(i => i.Type != MediaSourceType.Placeholder)
.ToList();
}

View File

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

View File

@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Fourth;
public override ResolverPriority Priority => ResolverPriority.Fifth;
public MultiItemResolverResult ResolveMultiple(
Folder parent,
@@ -201,6 +201,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue;
}
if (resolvedItem.Files.Count == 0)
{
continue;
}
var firstMedia = resolvedItem.Files[0];
var libraryItem = new T

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
// 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)))
{
return new Playlist
@@ -63,7 +63,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
Path = args.Path,
Name = Path.GetFileNameWithoutExtension(args.Path),
IsInMixedFolder = true
IsInMixedFolder = true,
PlaylistMediaType = MediaType.Audio
};
}
}

View File

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

View File

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

View File

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

View File

@@ -1860,7 +1860,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -1924,7 +1925,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -2608,7 +2610,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Locations = new string[] { customPath },
Name = "Recorded Movies",
CollectionType = CollectionType.Movies
CollectionType = CollectionTypeOptions.Movies
};
}
@@ -2619,7 +2621,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Locations = new string[] { customPath },
Name = "Recorded Shows",
CollectionType = CollectionType.TvShows
CollectionType = CollectionTypeOptions.TvShows
};
}
}

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
@@ -11,9 +14,11 @@ using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -33,6 +38,21 @@ namespace Emby.Server.Implementations.Plugins
private readonly IList<LocalPlugin> _plugins;
private readonly Version _minimumVersion;
private IHttpClientFactory? _httpClientFactory;
private IHttpClientFactory HttpClientFactory
{
get
{
if (_httpClientFactory == null)
{
_httpClientFactory = _appHost.Resolve<IHttpClientFactory>();
}
return _httpClientFactory;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
@@ -112,8 +132,6 @@ namespace Emby.Server.Implementations.Plugins
{
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)
@@ -122,6 +140,20 @@ namespace Emby.Server.Implementations.Plugins
ChangePluginState(plugin, PluginStatus.Malfunctioned);
continue;
}
catch (TypeLoadException ex) // Undocumented exception
{
_logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
ChangePluginState(plugin, PluginStatus.NotSupported);
continue;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file);
ChangePluginState(plugin, PluginStatus.Malfunctioned);
continue;
}
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
yield return assembly;
@@ -320,32 +352,74 @@ namespace Emby.Server.Implementations.Plugins
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>
/// <inheritdoc/>
public bool SaveManifest(PluginManifest manifest, string path)
{
if (manifest == null)
try
{
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
File.WriteAllText(Path.Combine(path, "meta.json"), data);
return true;
}
catch (ArgumentException e)
{
_logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path);
return false;
}
}
/// <inheritdoc/>
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
{
if (packageInfo == null)
{
return false;
}
try
var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
var imagePath = string.Empty;
if (!string.IsNullOrEmpty(packageInfo.ImageUrl))
{
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8);
return true;
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
await using var fileStream = File.OpenWrite(imagePath);
try
{
await using var downloadStream = await HttpClientFactory
.CreateClient(NamedClient.Default)
.GetStreamAsync(url)
.ConfigureAwait(false);
await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
imagePath = string.Empty;
}
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception e)
#pragma warning restore CA1031 // Do not catch general exception types
var manifest = new PluginManifest
{
_logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
return false;
}
Category = packageInfo.Category,
Changelog = versionInfo.Changelog ?? string.Empty,
Description = packageInfo.Description,
Id = new Guid(packageInfo.Id),
Name = packageInfo.Name,
Overview = packageInfo.Overview,
Owner = packageInfo.Owner,
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
Version = versionInfo.Version,
Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
AutoUpdate = true,
ImagePath = imagePath
};
return SaveManifest(manifest, path);
}
/// <summary>
@@ -374,7 +448,7 @@ namespace Emby.Server.Implementations.Plugins
private LocalPlugin? GetPluginByAssembly(Assembly assembly)
{
// Find which plugin it is by the path.
return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(assembly.Location), StringComparison.Ordinal));
return _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location, StringComparer.Ordinal));
}
/// <summary>
@@ -398,7 +472,7 @@ namespace Emby.Server.Implementations.Plugins
if (plugin == null)
{
// Create a dummy record for the providers.
// TODO: remove this code, if all provided have been released as separate plugins.
// TODO: remove this code once all provided have been released as separate plugins.
plugin = new LocalPlugin(
instance.AssemblyFilePath,
true,
@@ -421,15 +495,17 @@ namespace Emby.Server.Implementations.Plugins
{
plugin.Instance = instance;
var manifest = plugin.Manifest;
var pluginStr = plugin.Instance.Version.ToString();
var pluginStr = instance.Version.ToString();
bool changed = false;
if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal))
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;
}
@@ -559,7 +635,7 @@ namespace Emby.Server.Implementations.Plugins
// Auto-create a plugin manifest, so we can disable it, if it fails to load.
manifest = new PluginManifest
{
Status = PluginStatus.Restart,
Status = PluginStatus.Active,
Name = metafile,
AutoUpdate = false,
Id = metafile.GetMD5(),

View File

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

View File

@@ -1310,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)
@@ -1456,7 +1456,12 @@ namespace Emby.Server.Implementations.Session
throw new SecurityException("Unknown quick connect token");
}
request.UserId = result.Items[0].UserId;
var info = result.Items[0];
request.UserId = info.UserId;
// There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token.
_authRepo.Delete(info);
return AuthenticateNewSessionInternal(request, false);
}
@@ -1538,23 +1543,26 @@ namespace Emby.Server.Implementations.Session
Limit = 1
}).Items.FirstOrDefault();
var allExistingForDevice = _authRepo.Get(
new AuthenticationInfoQuery
{
DeviceId = deviceId
}).Items;
foreach (var auth in allExistingForDevice)
if (!string.IsNullOrEmpty(deviceId))
{
if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
var allExistingForDevice = _authRepo.Get(
new AuthenticationInfoQuery
{
DeviceId = deviceId
}).Items;
foreach (var auth in allExistingForDevice)
{
try
if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
{
Logout(auth);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while logging out.");
try
{
Logout(auth);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while logging out.");
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
@@ -192,17 +193,12 @@ namespace Emby.Server.Implementations.Updates
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);
}
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
}
// Remove versions with a target abi that is greater then the current application version.
// Remove versions with a target ABI greater then the current application version.
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
{
package.Versions.RemoveAt(i);
@@ -294,7 +290,8 @@ namespace Emby.Server.Implementations.Updates
Name = package.Name,
Version = v.VersionNumber,
SourceUrl = v.SourceUrl,
Checksum = v.Checksum
Checksum = v.Checksum,
PackageInfo = package
};
}
}
@@ -504,7 +501,8 @@ namespace Emby.Server.Implementations.Updates
var plugins = _pluginManager.Plugins;
foreach (var plugin in plugins)
{
if (plugin.Manifest?.AutoUpdate == false)
// Don't auto update when plugin marked not to, or when it's disabled.
if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
{
continue;
}
@@ -519,7 +517,7 @@ namespace Emby.Server.Implementations.Updates
}
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{
var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
@@ -571,24 +569,16 @@ namespace Emby.Server.Implementations.Updates
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_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
await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false);
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
return plugin != null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -87,6 +87,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -133,6 +134,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
@@ -312,6 +314,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpPost("Items/{itemId}/Images/{imageType}")]
[Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@@ -346,6 +349,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
[Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@@ -388,7 +392,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] int newIndex)
[FromQuery, Required] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -476,6 +480,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
@@ -505,6 +511,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] string? tag,
[FromQuery] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
@@ -535,6 +543,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -556,6 +566,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
@@ -585,6 +597,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] string? tag,
[FromQuery] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
@@ -614,6 +628,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -634,6 +650,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
@@ -663,6 +681,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromRoute, Required] string tag,
[FromQuery] bool? cropWhitespace,
[FromRoute, Required] ImageFormat format,
@@ -693,6 +713,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -717,6 +739,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -737,7 +761,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetArtistImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -746,6 +770,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -772,6 +798,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -796,6 +824,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -816,7 +846,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -825,6 +855,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -851,6 +883,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -876,6 +910,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -896,7 +932,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -905,6 +941,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -930,6 +968,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -954,6 +994,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -974,7 +1016,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetMusicGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -983,6 +1025,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1009,6 +1053,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1034,6 +1080,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1054,7 +1102,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1063,6 +1111,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1088,6 +1138,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1112,6 +1164,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1132,7 +1186,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetPersonImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1141,6 +1195,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1167,6 +1223,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1192,6 +1250,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1212,7 +1272,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1221,6 +1281,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1246,6 +1308,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1270,6 +1334,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1299,6 +1365,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1325,6 +1393,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1350,6 +1420,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1379,6 +1451,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1404,6 +1478,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1428,6 +1504,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1457,6 +1535,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1500,6 +1580,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1526,6 +1608,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
@@ -1555,6 +1639,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
@@ -1597,6 +1683,8 @@ namespace Jellyfin.Api.Controllers
width,
height,
quality,
fillWidth,
fillHeight,
cropWhitespace,
addPlayedIndicator,
blur,
@@ -1681,6 +1769,8 @@ namespace Jellyfin.Api.Controllers
int? width,
int? height,
int? quality,
int? fillWidth,
int? fillHeight,
bool? cropWhitespace,
bool? addPlayedIndicator,
int? blur,
@@ -1744,11 +1834,13 @@ namespace Jellyfin.Api.Controllers
item,
itemId,
imageIndex,
height,
maxHeight,
maxWidth,
quality,
width,
height,
maxWidth,
maxHeight,
fillWidth,
fillHeight,
quality,
addPlayedIndicator,
percentPlayed,
unplayedCount,
@@ -1843,11 +1935,13 @@ namespace Jellyfin.Api.Controllers
BaseItem? item,
Guid itemId,
int? index,
int? height,
int? maxHeight,
int? maxWidth,
int? quality,
int? width,
int? height,
int? maxWidth,
int? maxHeight,
int? fillWidth,
int? fillHeight,
int? quality,
bool? addPlayedIndicator,
double? percentPlayed,
int? unplayedCount,
@@ -1876,6 +1970,8 @@ namespace Jellyfin.Api.Controllers
ItemId = itemId,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
FillHeight = fillHeight,
FillWidth = fillWidth,
Quality = quality ?? 100,
Width = width,
AddPlayedIndicator = addPlayedIndicator ?? false,

View File

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

View File

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

View File

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

View File

@@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
[FromQuery] string? sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
@@ -184,7 +184,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -247,8 +247,13 @@ namespace Jellyfin.Api.Controllers
folder = _libraryManager.GetUserRootFolder();
}
if (folder is IHasCollectionType hasCollectionType
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
string? collectionType = null;
if (folder is IHasCollectionType hasCollectionType)
{
collectionType = hasCollectionType.CollectionType;
}
if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{
recursive = true;
includeItemTypes = new[] { "Playlist" };
@@ -271,10 +276,11 @@ namespace Jellyfin.Api.Controllers
}
}
if (!(item is UserRootFolder)
if (item is not UserRootFolder
&& !isInEnabledFolder
&& !user.HasPermission(PermissionKind.EnableAllFolders)
&& !user.HasPermission(PermissionKind.EnableAllChannels))
&& !user.HasPermission(PermissionKind.EnableAllChannels)
&& !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
@@ -608,7 +614,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
[FromQuery] string? sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
@@ -617,7 +623,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,

View File

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

View File

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

View File

@@ -553,8 +553,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isSports,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? sortBy,
[FromQuery] string? sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool? enableImages,

View File

@@ -83,6 +83,7 @@ namespace Jellyfin.Api.Controllers
/// </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="itemId">The item id.</param>
/// <param name="userId">The user id.</param>
@@ -106,20 +107,20 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? mediaSourceId,
[FromQuery] string? liveStreamId,
[FromQuery] bool? autoOpenLiveStream,
[FromQuery] bool? enableDirectPlay,
[FromQuery] bool? enableDirectStream,
[FromQuery] bool? enableTranscoding,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery, ParameterObsolete] Guid? userId,
[FromQuery, ParameterObsolete] int? maxStreamingBitrate,
[FromQuery, ParameterObsolete] long? startTimeTicks,
[FromQuery, ParameterObsolete] int? audioStreamIndex,
[FromQuery, ParameterObsolete] int? subtitleStreamIndex,
[FromQuery, ParameterObsolete] int? maxAudioChannels,
[FromQuery, ParameterObsolete] string? mediaSourceId,
[FromQuery, ParameterObsolete] string? liveStreamId,
[FromQuery, ParameterObsolete] bool? autoOpenLiveStream,
[FromQuery, ParameterObsolete] bool? enableDirectPlay,
[FromQuery, ParameterObsolete] bool? enableDirectStream,
[FromQuery, ParameterObsolete] bool? enableTranscoding,
[FromQuery, ParameterObsolete] bool? allowVideoStreamCopy,
[FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);

View File

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

View File

@@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Repositories")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
{
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
_serverConfigurationManager.SaveConfiguration();

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -57,6 +58,7 @@ namespace Jellyfin.Api.Controllers
/// </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>
@@ -70,10 +72,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromQuery] string? name,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] IReadOnlyList<Guid> ids,
[FromQuery] Guid? userId,
[FromQuery] string? mediaType,
[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)

View File

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

View File

@@ -300,9 +300,7 @@ namespace Jellyfin.Api.Controllers
}
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))
if (plugin.Manifest.ImagePath == null || !System.IO.File.Exists(imagePath))
{
return NotFound();
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -67,6 +67,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</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>
[HttpGet("NextUp")]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -81,7 +82,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool disableFirstEpisode = false)
{
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
@@ -95,7 +97,8 @@ namespace Jellyfin.Api.Controllers
SeriesId = seriesId,
StartIndex = startIndex,
UserId = userId ?? Guid.Empty,
EnableTotalRecordCount = enableTotalRecordCount
EnableTotalRecordCount = enableTotalRecordCount,
DisableFirstEpisode = disableFirstEpisode
},
options);
@@ -267,7 +270,7 @@ namespace Jellyfin.Api.Controllers
if (startItemId.HasValue)
{
episodes = episodes
.SkipWhile(i => startItemId.Value.Equals(i.Id))
.SkipWhile(i => !startItemId.Value.Equals(i.Id))
.ToList();
}

View File

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

View File

@@ -509,14 +509,14 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Redeems a forgot password pin.
/// </summary>
/// <param name="pin">The pin.</param>
/// <param name="forgotPasswordPinRequest">The forgot password pin request containing the entered pin.</param>
/// <response code="200">Pin reset process started.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
[HttpPost("ForgotPassword/Pin")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string? pin)
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
{
var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
return result;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -8,7 +9,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
@@ -25,35 +25,27 @@ namespace Jellyfin.Api.Helpers
/// <param name="sortBy">Sort By. Comma delimited string.</param>
/// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
/// <returns>Order By.</returns>
public static ValueTuple<string, SortOrder>[] GetOrderBy(string? sortBy, string? requestedSortOrder)
public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
{
var val = sortBy;
if (string.IsNullOrEmpty(val))
if (sortBy.Count == 0)
{
return Array.Empty<ValueTuple<string, SortOrder>>();
}
var vals = val.Split(',');
if (string.IsNullOrWhiteSpace(requestedSortOrder))
var result = new (string, SortOrder)[sortBy.Count];
var i = 0;
// Add elements which have a SortOrder specified
for (; i < requestedSortOrder.Count; i++)
{
requestedSortOrder = "Ascending";
result[i] = (sortBy[i], requestedSortOrder[i]);
}
var sortOrders = requestedSortOrder.Split(',');
var result = new ValueTuple<string, SortOrder>[vals.Length];
for (var i = 0; i < vals.Length; i++)
// Add remaining elements with the first specified SortOrder
// or the default one if no SortOrders are specified
var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
for (; i < sortBy.Count; i++)
{
var sortOrderIndex = sortOrders.Length > i ? i : 0;
var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
? SortOrder.Descending
: SortOrder.Ascending;
result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
result[i] = (sortBy[i], order);
}
return result;

View File

@@ -210,6 +210,7 @@ namespace Jellyfin.Api.Helpers
&& !state.VideoRequest.MaxHeight.HasValue;
if (isVideoResolutionNotRequested
&& state.VideoStream != null
&& state.VideoRequest.VideoBitRate.HasValue
&& state.VideoStream.BitRate.HasValue
&& state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
@@ -507,17 +508,15 @@ namespace Jellyfin.Api.Helpers
private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
{
var headers = request.Headers;
if (!string.IsNullOrWhiteSpace(deviceProfileId))
{
state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
}
else if (!string.IsNullOrWhiteSpace(deviceProfileId))
{
var caps = deviceManager.GetCapabilities(deviceProfileId);
state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
if (state.DeviceProfile == null)
{
var caps = deviceManager.GetCapabilities(deviceProfileId);
state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
}
}
var profile = state.DeviceProfile;

View File

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

View File

@@ -69,7 +69,7 @@ namespace Jellyfin.Api.ModelBinders
}
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)
{
_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)
{
_logger.LogWarning(e, "Error converting value.");
_logger.LogDebug(e, "Error converting value.");
}
}

View File

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

View File

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

View File

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

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