Compare commits

...

117 Commits

Author SHA1 Message Date
Joshua M. Boniface 595a68b822 Bump version for 10.3.7 2019-07-24 10:48:35 -04:00
Anthony Lavado c5d9480313 Merge pull request #1552 from cvium/fix_livetv_v2
Disable buffering in HttpClient as it causes big requests to timeout
2019-07-22 02:28:38 -04:00
Claus Vium dadfc09c01 Add HttpCompletionOption.ResponseHeadersRead to the buffering option to avoid potentially having 2 copies in memory 2019-07-20 13:36:59 +02:00
Claus Vium 886c88576c Use HttpCompletionOption.ResponseHeadersRead and resort to Content-Length header for non-buffered content 2019-07-19 23:22:30 +02:00
Joshua M. Boniface cf2f5b2026 Merge pull request #1538 from joshuaboniface/epg
Try to fix XmlTvListingsProvider
2019-07-14 17:09:00 -04:00
Joshua M. Boniface 135c16c721 Merge pull request #1537 from joshuaboniface/contenttype
Properly set content type
2019-07-14 17:08:54 -04:00
Bond_009 5d9fa06675 Cleanup 2019-07-13 17:18:39 -04:00
Bond_009 b294b802a8 Try to fix XmlTvListingsProvider 2019-07-13 17:18:27 -04:00
Bond_009 7bb504d491 Create a new HttpMethod from the function name 2019-07-13 17:12:06 -04:00
Bond_009 b1bd062709 Properly set content type 2019-07-13 17:12:06 -04:00
Joshua M. Boniface 4f17ed961e Merge pull request #1514 from Bond-009/httpclient2
Fix issues with HttpClientManager
2019-07-06 17:01:35 -04:00
Bond_009 5fc4ad6c4e Address comments 2019-07-06 20:04:45 +02:00
Bond_009 b117b364f2 Remove duplicate code 2019-07-06 20:04:45 +02:00
Bond_009 3603c64fa6 Use HttpResponseHeaders instead of a dictionary 2019-07-06 20:04:45 +02:00
Bond_009 d405a400aa Fixes issues with HttpClientManager 2019-07-06 20:04:42 +02:00
Joshua M. Boniface 54c6f02ebb Merge pull request #1455 from ferferga/release-10.3.z
Vacuum databases at startup
2019-07-06 13:57:18 -04:00
Joshua M. Boniface b3f9d04501 Bump version for 10.3.6 2019-07-06 13:34:38 -04:00
Erwin de Haan acf52b9b55 Cleanup extra spaces. 2019-07-04 20:55:49 +02:00
Erwin de Haan 7587fe56d8 Moved VACUUM down to the end of the list. 2019-07-04 20:54:57 +02:00
Anthony Lavado ab34a95142 Merge pull request #1473 from DrPandemic/apply-deprecation-WebRequest-on-10.3.z
Apply deprecation web request on 10.3.z
2019-07-04 14:51:24 -04:00
Andrew Rabert 0ee40cb636 Merge pull request #1495 from joshuaboniface/better-restart-script
Add nicer restart script
2019-06-29 19:48:54 -04:00
Joshua M. Boniface 62105c249f Use which to find the service binary path 2019-06-28 11:15:08 -04:00
Joshua M. Boniface a629f209b9 Make message wording more consistent 2019-06-28 11:06:55 -04:00
Joshua M. Boniface c08c0272b5 Add nicer restart script
The old restart script was buggy, as reported in #1320. This updated
script seems to work far more reliably and conforms to the existing
jellyfin-sudoers packages sudo configuration.
2019-06-27 18:05:03 -04:00
dkanada c52e8a2027 Merge pull request #1394 from joern-h/bugfix-issue1347
Check if an item is a child of an EnabledFolder
2019-06-26 01:19:34 -07:00
Jörn 1fd8164756 fix issue #1347 introduced in pr #930 2019-06-26 01:13:54 -07:00
dkanada 3c16c34386 Merge pull request #1485 from DrPandemic/fix-skia-segfault
Fix skia segfault
2019-06-25 12:44:29 -05:00
DrPandemic 394d96246b Check path before opening image 2019-06-24 20:13:07 -04:00
Anthony Lavado 084854d71d Merge pull request #1478 from cvium/fix_tvdb_again
Wait for the async authentication to finish when the JTW token expires
2019-06-22 02:08:46 -04:00
Claus Vium c2ab0ad641 Wait for the async authentication to finish when the JTW token expires 2019-06-21 19:08:04 +02:00
Claus Vium 7eb94e9674 Update MediaBrowser.Common/Net/IHttpClient.cs
Co-Authored-By: Bond-009 <bond.009@outlook.com>
2019-06-18 22:21:07 -04:00
Bond-009 0a5550b13d Remove more unused stuff 2019-06-18 22:20:34 -04:00
Bond-009 067200be83 Remove usage of depricated 'WebRequest'
Ref: https://docs.microsoft.com/en-us/dotnet/api/system.net.webrequest?view=netframework-4.7.2
2019-06-17 19:35:05 -04:00
ferferga b136f14084 Vacuum databases at startup 2019-06-10 11:31:38 +02:00
Joshua M. Boniface d5fe82314e Bump version for 10.3.5 2019-06-09 21:47:45 -04:00
Joshua M. Boniface 06834fefef Merge pull request #1447 from joshuaboniface/implement-invalidauth
Implement InvalidAuthProvider
2019-06-09 15:36:25 -04:00
Joshua M. Boniface 2946ae1009 Revert "Don't set a default reset provider"
This reverts commit c230d49d7c.

This reenables an edge case where an admin might want to reset, with
the default auth provider, the password of an externally-provided
user so they could "unlock" the account while it was failing. There
might be minor security implications to this, but the malicious
actor would need FS access to do it (as they would with any password
resets) so it's probably best to keep it as-is.

Removing this in the first place was due to a misunderstanding
anyways so no harm.
2019-06-09 15:29:43 -04:00
Joshua M. Boniface 4b8f735cb8 Remove superfluous conditional
This wasn't needed to prevent updating the policy on-disk from my
tests and can be removed as suggested by @Bond-009
2019-06-09 13:57:49 -04:00
Joshua M. Boniface c230d49d7c Don't set a default reset provider 2019-06-09 13:46:53 -04:00
Joshua M. Boniface 20e2cb2d86 Use SecurityException for auth failure 2019-06-09 13:45:51 -04:00
Joshua M. Boniface b70083f3b3 Apply suggestions from code review
Co-Authored-By: Claus Vium <cvium@users.noreply.github.com>
Co-Authored-By: Bond-009 <bond.009@outlook.com>
2019-06-09 13:41:14 -04:00
Joshua M. Boniface 74ef389879 Add nicer log message and comment 2019-06-09 11:07:35 -04:00
Joshua M. Boniface d78a55adb4 Implement InvalidAuthProvider
Implements the InvalidAuthProvider, which acts as a fallback if a
configured authentication provider, e.g. LDAP, is unavailable due
to a load failure or removal. Until the user or the authentication
plugin is corrected, this will cause users with the missing provider
to be locked out, while throwing errors in the logs about the issue.

Fixes #1445 part 2
2019-06-08 22:54:31 -04:00
Andrew Rabert 6f99ed3955 Merge pull request #1443 from jellyfin/qemu
Docker - Update arm* Dockerfiles for latest multiarch
2019-06-07 18:41:35 -04:00
Andrew Rabert 247a5e12ab Docker - Update arm* Dockerfiles for latest multiarch
Relates to this change https://github.com/multiarch/qemu-user-static/commit/7bdafb96ee59fde53d07a291f9446d2b8957d372
2019-06-07 00:00:14 -04:00
Joshua M. Boniface 855911333a Bump version for 10.3.4 2019-06-06 22:45:37 -04:00
Anthony Lavado 127bfc7d3b Merge pull request #1437 from pjeanjean/master
Fix issue #1436: media folders appear empty unless user has all libraries access
2019-06-06 17:21:20 -04:00
pjeanjean 7919dd81da Skip user permission checking for UserRootFolder
Fix #1436
UserRootFolders are used to represent virtual folders that exist outside
of libraries. As such, it doesn't make sense to check if a user has the
right to access their library (named `Media Folders`).
2019-06-06 08:30:56 +02:00
Anthony Lavado e1da046960 Merge pull request #1426 from jellyfin/cvium-fix-tvdb-refresh
Fix inverted comparison in the tvdb token refresh logic
2019-05-31 21:27:33 -04:00
Claus Vium a756026962 Fix inverted comparison in the tvdb token refresh logic 2019-05-31 07:24:52 +02:00
Anthony Lavado 75260a960b Merge pull request #1406 from DrPandemic/fix-pin-update
Format the PIN when updating it
2019-05-31 00:58:53 -04:00
DrPandemic 69ee49bee6 Format correctly the PIN when updating it 2019-05-25 13:46:55 -04:00
Joshua M. Boniface 1bf3a26a61 Bump version for 10.3.3 2019-05-17 23:12:21 -04:00
Joshua M. Boniface c5760b3a40 Merge pull request #1380 from bugfixin/dlna-photo-thumb-fix
Improve Photo rendering in DLNA
2019-05-17 22:55:43 -04:00
Joshua M. Boniface 4de8bf3295 Merge pull request #1338 from cvium/fix_extras
Enforce a specific folder structure for Extras to avoid misidentification
2019-05-17 09:00:22 -04:00
bugfixin 87c8f19f19 Move DLNA thumbnail element to after larger image elements 2019-05-16 18:00:38 +00:00
Claus Vium 0ef52c739e Review changes
Untested
2019-05-16 07:27:38 +02:00
Joshua M. Boniface 0a9a6b949c Merge pull request #1372 from DrPandemic/fix-pin
Fix broken pin in 10.3.z
2019-05-14 21:54:28 -04:00
DrPandemic c22068d6b1 Fix pin bug introduced in 10.3.z.
The issue is that the new easyPassword format prepends the hash
function. This PR extract the hash from "$SHA1$_hash_".
2019-05-11 19:53:34 -04:00
Joshua M. Boniface bbc1a86b57 Merge pull request #1306 from oddstr13/pr-sudoless-build-1
Move artifact chown inside docker to avoid sudo
2019-05-10 09:25:41 -04:00
Bond-009 cd83d80f2b Merge pull request #1294 from DrPandemic/fix-download-non-ascii
Fix non-ascii filename downloads
2019-05-09 17:30:19 +02:00
Jean-Samuel Aubry-Guzzi 12721eb7dd Fix non-ascii filename downloads
Follow https://tools.ietf.org/html/rfc5987#section-3.2.2 to encode
non-ascii filenames in HTTP Content-Disposition header.
2019-05-07 19:43:04 -04:00
Claus Vium b8a09339cd Enforce extras folder structure according to Emby's wiki 2019-05-02 08:14:00 +02:00
Odd Stråbø 3634d367c1 Move artifact chown inside docker to avoid sudo 2019-05-01 20:32:15 +02:00
Claus Vium c1daea0ec7 Change owner and parent id of extras to the main media item 2019-05-01 07:47:22 +02:00
Joshua Boniface e8196fed7c Bump version for 10.3.2 2019-04-30 20:18:54 -04:00
Joshua M. Boniface 477702fbb9 Merge pull request #1324 from joshuaboniface/arm64
Add arm64 packaging for Debuntu
2019-04-30 20:07:41 -04:00
Joshua M. Boniface 2216a271bb Merge pull request #1335 from Bond-009/ffmpeglimit
Limit amount of ffmpeg processes extracting images at once
2019-04-30 20:06:24 -04:00
Bond-009 91cd7d2f6b Limit amount of ffmpeg processes extracting images at once 2019-04-30 23:35:39 +02:00
Anthony Lavado 4e681d25d9 Merge pull request #1334 from Bond-009/musicbrainzfix
Iterate over IEnumerable before disposing
2019-04-30 16:56:54 -04:00
Bond-009 06cc2891de Merge pull request #1310 from bugfixin/progressivestreamingservice-unreachable
Remove unreachable code from BaseProgressiveStreamingService
2019-04-30 22:20:54 +02:00
Bond-009 682432f55a Iterate over IEnumerable before disposing 2019-04-30 22:18:40 +02:00
Anthony Lavado 7c4cb5ec58 Merge pull request #1333 from bugfixin/easypinfix
Fix incorrect hasPassword flag when easy pin set
2019-04-30 15:36:25 -04:00
bugfixin 1df73fdeba Fix incorrect hasPassword flag when easy pin set 2019-04-30 19:16:53 +00:00
Bond-009 21ba8a0593 Merge pull request #1332 from cvium/fix_tvdb_ep_provider
Make the TvdbEpisodeProvider class Public
2019-04-30 20:59:18 +02:00
Claus Vium 08ed52eb72 Make the TvdbEpisodeProvider class Public 2019-04-30 20:08:59 +02:00
Anthony Lavado 99700e1b95 Merge pull request #1327 from joshuaboniface/disco
Support libssl1.1 for Ubuntu Disco
2019-04-30 02:38:21 -04:00
Joshua M. Boniface 4e0be95368 Merge pull request #1305 from bugfixin/passwordless-form-encoded
Fix passwordless authentication with non-json content-types
2019-04-29 23:07:04 -04:00
Joshua Boniface c8a59c8343 Support libssl1.1 for Ubuntu Disco 2019-04-29 23:03:57 -04:00
Joshua Boniface 2b2a2ed708 Add arm64 packaging for Debuntu 2019-04-29 00:56:17 -04:00
bugfixin a827a2fbcc Remove unreachable code and const trySupportSeek within BaseProgressiveStreamingService 2019-04-25 19:14:33 +00:00
Anthony Lavado f97f6b8061 Merge pull request #1296 from Bond-009/fix1234
Fix #1234
2019-04-25 01:37:02 -04:00
bugfixin 844ea9d77e Don't coalesce empty strings to null in StringMapTypeDeserializer 2019-04-25 04:36:28 +00:00
Bond_009 71479286e9 Fix #1234 2019-04-24 19:56:57 +02:00
Claus Vium 28c2ac528d Re-add content length, semi revert of changes in #1010 (#1287)
* Re-add content length, semi revert of changes in #1010
2019-04-24 14:06:54 +02:00
Joshua Boniface 696a36b4a5 Update submodule for 10.3.1 2019-04-20 15:59:50 -04:00
Joshua Boniface 5fb4922c6f Bump version to 10.3.1 2019-04-20 14:24:40 -04:00
Joshua M. Boniface 3738f95871 Merge pull request #1258 from Bond-009/fixpluginload
Handle exception when loading unsupported assembly
2019-04-20 12:35:57 -04:00
Joshua M. Boniface 6797bdec04 Merge pull request #1264 from jellyfin/fix-empty-pw-migration
Fix comparison for empty password migration
2019-04-20 12:20:22 -04:00
Claus Vium 764c6d5461 Fix comparison for empty password migration 2019-04-20 17:50:34 +02:00
Bond-009 6973182ade Fix more possible exceptions 2019-04-20 17:47:11 +02:00
Bond-009 f62af07381 Handle exception when loading unsupported assembly
Fixes #1256
2019-04-20 17:47:11 +02:00
Joshua Boniface 46c37c0ae8 Bump version to 10.3.0 (release) 2019-04-19 14:25:29 -04:00
Joshua Boniface 4ad71766fc Merge branch 'translations' into release-10.3.z 2019-04-19 14:19:22 -04:00
Weblate 9d60cc8c66 Rename Chinese (Traditional) file 2019-04-19 13:59:04 -04:00
Libor Filípek 4a6243096a Translated using Weblate (Czech)
Currently translated at 100.0% (94 of 94 strings)

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/
2019-04-19 13:38:32 -04:00
Heldenkrieger01 10cbdc8e8e Translated using Weblate (Swiss German)
Currently translated at 100.0% (94 of 94 strings)

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gsw/
2019-04-19 12:09:39 -04:00
SaddFox 4886fc467c Translated using Weblate (Slovenian)
Currently translated at 100.0% (94 of 94 strings)

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sl/
2019-04-19 12:09:38 -04:00
WWWesten 8e2827cc39 Translated using Weblate (Kazakh)
Currently translated at 100.0% (94 of 94 strings)

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/kk/
2019-04-19 12:09:37 -04:00
Βασίλης Μουρατίδης 395d2e4917 Translated using Weblate (Greek)
Currently translated at 100.0% (94 of 94 strings)

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/
2019-04-19 12:09:37 -04:00
Dan Johansen d31f5229da Translated using Weblate (Danish)
Currently translated at 100.0% (94 of 94 strings)

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/
2019-04-19 12:09:37 -04:00
Libor Filípek d6622818dc Translated using Weblate (Czech)
Currently translated at 100.0% (94 of 94 strings)

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/
2019-04-19 12:09:37 -04:00
tinganhsu ba684d6d3a Translated using Weblate (Chinese (Traditional))
Currently translated at 96.8% (91 of 94 strings)

Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/
2019-04-19 12:09:36 -04:00
tinganhsu 90c04a4640 Added translation using Weblate (Chinese (Traditional)) 2019-04-19 00:32:52 -04:00
Joshua M. Boniface 06a1e1f166 Merge pull request #1244 from joshuaboniface/hotfix-authapi
Hotfix authapi
2019-04-18 17:58:54 -04:00
Joshua Boniface 31ad366aa9 Implemented suggested conditional 2019-04-18 10:24:08 -04:00
Joshua Boniface 10f33b0273 Update conditional to be correct 2019-04-18 09:31:30 -04:00
Joshua Boniface eaa1ac8013 Apparently strings can't be !'d 2019-04-17 22:49:17 -04:00
Joshua Boniface ca3bb308b3 Add the proper Class too 2019-04-17 22:46:26 -04:00
Joshua Boniface e790f024c2 Return MethodNotAllowedException if Pw is not set
Don't accept pre-hashed (not-plaintext) passwords as the auth
provider no longer supports this due to sha1+salting the passwords
in the database.
2019-04-17 22:33:00 -04:00
Joshua Boniface 250e0c75df Add MethodNotAllowedException with code 405 2019-04-17 22:31:06 -04:00
Joshua M. Boniface cde7375049 Merge pull request #1242 from Bond-009/metadata
Fix metadata path save
2019-04-17 09:26:04 -04:00
Bond_009 c7fedfbca3 Fix metadata path save 2019-04-17 15:09:31 +02:00
Bond-009 f520831025 Merge pull request #1239 from anthonylavado/fix-ident
Clean up UDP responders, and move ProductName to Public endpoint
2019-04-16 11:56:38 +02:00
Anthony Lavado 2f0719a883 Move the definition of ProductName to the correct class
Missed moving this from one class to the other.
2019-04-16 01:38:00 -04:00
Anthony Lavado 34ab99caf1 Move the ProductName to the public endpoint
Moves the ProductName field over from the private system/info point to
the public one, for easier identification
2019-04-16 01:16:02 -04:00
Anthony Lavado b2f94c0e40 Remove the old message responders
Leaves only an answer to "Who is Jellyfin", removing older ones for
EmbyServer and MediaBrowser_v2.
2019-04-15 00:21:14 -04:00
92 changed files with 1437 additions and 983 deletions
+4 -1
View File
@@ -23,7 +23,10 @@
- [fruhnow](https://github.com/fruhnow) - [fruhnow](https://github.com/fruhnow)
- [Lynxy](https://github.com/Lynxy) - [Lynxy](https://github.com/Lynxy)
- [fasheng](https://github.com/fasheng) - [fasheng](https://github.com/fasheng)
- [ploughpuff](https://github.com/ploughpuff) - [ploughpuff](https://github.com/ploughpuff)
- [pjeanjean](https://github.com/pjeanjean)
- [DrPandemic](https://github.com/drpandemic)
- [joern-h](https://github.com/joern-h)
# Emby Contributors # Emby Contributors
+1 -1
View File
@@ -21,7 +21,7 @@ RUN apt-get update \
COPY --from=ffmpeg / / COPY --from=ffmpeg / /
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.3.0-rc2 ARG JELLYFIN_WEB_VERSION=10.3.7
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \ && rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
+3 -7
View File
@@ -3,11 +3,6 @@
ARG DOTNET_VERSION=3.0 ARG DOTNET_VERSION=3.0
FROM multiarch/qemu-user-static:x86_64-arm as qemu
FROM alpine as qemu_extract
COPY --from=qemu /usr/bin qemu-arm-static.tar.gz
RUN tar -xzvf qemu-arm-static.tar.gz
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
@@ -21,8 +16,9 @@ RUN bash -c "source deployment/common.build.sh && \
build_jellyfin Jellyfin.Server Release linux-arm /jellyfin" build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
FROM multiarch/qemu-user-static:x86_64-arm as qemu
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7 FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7
COPY --from=qemu_extract qemu-arm-static /usr/bin COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
@@ -30,7 +26,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media && chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.3.0-rc2 ARG JELLYFIN_WEB_VERSION=10.3.7
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \ && rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
+3 -8
View File
@@ -3,12 +3,6 @@
ARG DOTNET_VERSION=3.0 ARG DOTNET_VERSION=3.0
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM alpine as qemu_extract
COPY --from=qemu /usr/bin qemu-aarch64-static.tar.gz
RUN tar -xzvf qemu-aarch64-static.tar.gz
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
@@ -22,8 +16,9 @@ RUN bash -c "source deployment/common.build.sh && \
build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin" build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8 FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8
COPY --from=qemu_extract qemu-aarch64-static /usr/bin COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
@@ -31,7 +26,7 @@ RUN apt-get update \
&& chmod 777 /cache /config /media && chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.3.0-rc2 ARG JELLYFIN_WEB_VERSION=10.3.7
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \ && rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
+3 -2
View File
@@ -920,8 +920,6 @@ namespace Emby.Dlna.Didl
} }
} }
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
{ {
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG"); AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
@@ -930,6 +928,9 @@ namespace Emby.Dlna.Didl
AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG"); AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG");
AddImageResElement(item, writer, 160, 160, "png", "PNG_TN"); AddImageResElement(item, writer, 160, 160, "png", "PNG_TN");
} }
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
} }
private void AddEmbeddedImageAsCover(string name, XmlWriter writer) private void AddEmbeddedImageAsCover(string name, XmlWriter writer)
+11 -12
View File
@@ -34,16 +34,13 @@ namespace Emby.Dlna.PlayTo
{ {
var cancellationToken = CancellationToken.None; var cancellationToken = CancellationToken.None;
using (var response = await PostSoapDataAsync(NormalizeServiceUrl(baseUrl, service.ControlUrl), "\"" + service.ServiceType + "#" + command + "\"", postData, header, logRequest, cancellationToken) var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
using (var response = await PostSoapDataAsync(url, '\"' + service.ServiceType + '#' + command + '\"', postData, header, logRequest, cancellationToken)
.ConfigureAwait(false)) .ConfigureAwait(false))
using (var stream = response.Content)
using (var reader = new StreamReader(stream, Encoding.UTF8))
{ {
using (var stream = response.Content) return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
{
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
}
}
} }
} }
@@ -121,15 +118,18 @@ namespace Emby.Dlna.PlayTo
} }
} }
private Task<HttpResponseInfo> PostSoapDataAsync(string url, private Task<HttpResponseInfo> PostSoapDataAsync(
string url,
string soapAction, string soapAction,
string postData, string postData,
string header, string header,
bool logRequest, bool logRequest,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (!soapAction.StartsWith("\"")) if (soapAction[0] != '\"')
soapAction = "\"" + soapAction + "\""; {
soapAction = '\"' + soapAction + '\"';
}
var options = new HttpRequestOptions var options = new HttpRequestOptions
{ {
@@ -155,7 +155,6 @@ namespace Emby.Dlna.PlayTo
} }
options.RequestContentType = "text/xml"; options.RequestContentType = "text/xml";
options.AppendCharsetToMimeType = true;
options.RequestContent = postData; options.RequestContent = postData;
return _httpClient.Post(options); return _httpClient.Post(options);
+53 -20
View File
@@ -6,6 +6,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
@@ -429,7 +430,7 @@ namespace Emby.Server.Implementations
/// Gets the current application user agent /// Gets the current application user agent
/// </summary> /// </summary>
/// <value>The application user agent.</value> /// <value>The application user agent.</value>
public string ApplicationUserAgent => Name.Replace(' ','-') + "/" + ApplicationVersion; public string ApplicationUserAgent => Name.Replace(' ','-') + '/' + ApplicationVersion;
/// <summary> /// <summary>
/// Gets the email address for use within a comment section of a user agent field. /// Gets the email address for use within a comment section of a user agent field.
@@ -689,11 +690,6 @@ namespace Emby.Server.Implementations
await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false); await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false);
} }
protected virtual IHttpClient CreateHttpClient()
{
return new HttpClientManager.HttpClientManager(ApplicationPaths, LoggerFactory, FileSystemManager, () => ApplicationUserAgent);
}
public static IStreamHelper StreamHelper { get; set; } public static IStreamHelper StreamHelper { get; set; }
/// <summary> /// <summary>
@@ -719,7 +715,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(FileSystemManager); serviceCollection.AddSingleton(FileSystemManager);
serviceCollection.AddSingleton<TvDbClientManager>(); serviceCollection.AddSingleton<TvDbClientManager>();
HttpClient = CreateHttpClient(); HttpClient = new HttpClientManager.HttpClientManager(
ApplicationPaths,
LoggerFactory.CreateLogger<HttpClientManager.HttpClientManager>(),
FileSystemManager,
() => ApplicationUserAgent);
serviceCollection.AddSingleton(HttpClient); serviceCollection.AddSingleton(HttpClient);
serviceCollection.AddSingleton(NetworkManager); serviceCollection.AddSingleton(NetworkManager);
@@ -1167,7 +1167,7 @@ namespace Emby.Server.Implementations
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error loading plugin {pluginName}", plugin.GetType().FullName); Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName);
return null; return null;
} }
@@ -1181,10 +1181,32 @@ namespace Emby.Server.Implementations
{ {
Logger.LogInformation("Loading assemblies"); Logger.LogInformation("Loading assemblies");
AllConcreteTypes = GetComposablePartAssemblies() AllConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
.SelectMany(x => x.ExportedTypes) }
.Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
.ToArray(); private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{
foreach (var ass in assemblies)
{
Type[] exportedTypes;
try
{
exportedTypes = ass.GetExportedTypes();
}
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
continue;
}
foreach (Type type in exportedTypes)
{
if (type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
{
yield return type;
}
}
}
} }
private CertificateInfo CertificateInfo { get; set; } private CertificateInfo CertificateInfo { get; set; }
@@ -1348,8 +1370,19 @@ namespace Emby.Server.Implementations
{ {
foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories)) foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
{ {
Logger.LogInformation("Loading assembly {Path}", file); Assembly plugAss;
yield return Assembly.LoadFrom(file); try
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss;
} }
} }
@@ -1425,7 +1458,6 @@ namespace Emby.Server.Implementations
HasPendingRestart = HasPendingRestart, HasPendingRestart = HasPendingRestart,
IsShuttingDown = IsShuttingDown, IsShuttingDown = IsShuttingDown,
Version = ApplicationVersion, Version = ApplicationVersion,
ProductName = ApplicationProductName,
WebSocketPortNumber = HttpPort, WebSocketPortNumber = HttpPort,
CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(), CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(),
Id = SystemId, Id = SystemId,
@@ -1482,6 +1514,7 @@ namespace Emby.Server.Implementations
return new PublicSystemInfo return new PublicSystemInfo
{ {
Version = ApplicationVersion, Version = ApplicationVersion,
ProductName = ApplicationProductName,
Id = SystemId, Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystem = OperatingSystem.Id.ToString(),
WanAddress = wanAddress, WanAddress = wanAddress,
@@ -1527,12 +1560,12 @@ namespace Emby.Server.Implementations
LogErrorResponseBody = false, LogErrorResponseBody = false,
LogErrors = false, LogErrors = false,
LogRequest = false, LogRequest = false,
TimeoutMs = 10000,
BufferContent = false, BufferContent = false,
CancellationToken = cancellationToken CancellationToken = cancellationToken
}).ConfigureAwait(false)) }).ConfigureAwait(false))
{ {
return GetWanApiUrl(response.ReadToEnd().Trim()); string res = await response.ReadToEndAsync().ConfigureAwait(false);
return GetWanApiUrl(res.Trim());
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -1684,15 +1717,15 @@ namespace Emby.Server.Implementations
LogErrorResponseBody = false, LogErrorResponseBody = false,
LogErrors = LogPing, LogErrors = LogPing,
LogRequest = LogPing, LogRequest = LogPing,
TimeoutMs = 5000,
BufferContent = false, BufferContent = false,
CancellationToken = cancellationToken CancellationToken = cancellationToken
}, "POST").ConfigureAwait(false)) }, HttpMethod.Post).ConfigureAwait(false))
{ {
using (var reader = new StreamReader(response.Content)) using (var reader = new StreamReader(response.Content))
{ {
var result = reader.ReadToEnd(); var result = await reader.ReadToEndAsync().ConfigureAwait(false);
var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
_validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
@@ -74,23 +74,14 @@ namespace Emby.Server.Implementations.Configuration
/// </summary> /// </summary>
private void UpdateMetadataPath() private void UpdateMetadataPath()
{ {
string metadataPath;
if (string.IsNullOrWhiteSpace(Configuration.MetadataPath)) if (string.IsNullOrWhiteSpace(Configuration.MetadataPath))
{ {
metadataPath = GetInternalMetadataPath(); ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
} }
else else
{ {
metadataPath = Path.Combine(Configuration.MetadataPath, "metadata"); ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Configuration.MetadataPath;
} }
((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath;
}
private string GetInternalMetadataPath()
{
return Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
} }
/// <summary> /// <summary>
@@ -223,6 +223,8 @@ namespace Emby.Server.Implementations.Data
"pragma temp_store = file" "pragma temp_store = file"
}); });
} }
// Configuration and pragmas can affect VACUUM so it needs to be last.
queries.Add("VACUUM");
db.ExecuteAll(string.Join(";", queries)); db.ExecuteAll(string.Join(";", queries));
Logger.LogInformation("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First()); Logger.LogInformation("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First());
@@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Data
{ {
// If the user password is the sha1 hash of the empty string, remove it // If the user password is the sha1 hash of the empty string, remove it
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
|| !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)) && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
{ {
continue; continue;
} }
@@ -1,18 +0,0 @@
using System;
using System.Net.Http;
namespace Emby.Server.Implementations.HttpClientManager
{
/// <summary>
/// Class HttpClientInfo
/// </summary>
public class HttpClientInfo
{
/// <summary>
/// Gets or sets the last timeout.
/// </summary>
/// <value>The last timeout.</value>
public DateTime LastTimeout { get; set; }
public HttpClient HttpClient { get; set; }
}
}
@@ -1,12 +1,9 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Cache; using System.Net.Http;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@@ -24,30 +21,24 @@ namespace Emby.Server.Implementations.HttpClientManager
/// </summary> /// </summary>
public class HttpClientManager : IHttpClient public class HttpClientManager : IHttpClient
{ {
/// <summary>
/// When one request to a host times out, we'll ban all other requests for this period of time, to prevent scans from stalling
/// </summary>
private const int TimeoutSeconds = 30;
/// <summary>
/// The _logger
/// </summary>
private readonly ILogger _logger; private readonly ILogger _logger;
/// <summary>
/// The _app paths
/// </summary>
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly Func<string> _defaultUserAgentFn; private readonly Func<string> _defaultUserAgentFn;
/// <summary>
/// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
/// DON'T dispose it after use.
/// </summary>
/// <value>The HTTP clients.</value>
private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HttpClientManager" /> class. /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
/// </summary> /// </summary>
public HttpClientManager( public HttpClientManager(
IApplicationPaths appPaths, IApplicationPaths appPaths,
ILoggerFactory loggerFactory, ILogger<HttpClientManager> logger,
IFileSystem fileSystem, IFileSystem fileSystem,
Func<string> defaultUserAgentFn) Func<string> defaultUserAgentFn)
{ {
@@ -55,46 +46,33 @@ namespace Emby.Server.Implementations.HttpClientManager
{ {
throw new ArgumentNullException(nameof(appPaths)); throw new ArgumentNullException(nameof(appPaths));
} }
if (loggerFactory == null)
if (logger == null)
{ {
throw new ArgumentNullException(nameof(loggerFactory)); throw new ArgumentNullException(nameof(logger));
} }
_logger = loggerFactory.CreateLogger("HttpClient"); _logger = logger;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_appPaths = appPaths; _appPaths = appPaths;
_defaultUserAgentFn = defaultUserAgentFn; _defaultUserAgentFn = defaultUserAgentFn;
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
ServicePointManager.Expect100Continue = false;
} }
/// <summary> /// <summary>
/// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests. /// Gets the correct http client for the given url.
/// DON'T dispose it after use.
/// </summary> /// </summary>
/// <value>The HTTP clients.</value> /// <param name="url">The url.</param>
private readonly ConcurrentDictionary<string, HttpClientInfo> _httpClients = new ConcurrentDictionary<string, HttpClientInfo>();
/// <summary>
/// Gets
/// </summary>
/// <param name="host">The host.</param>
/// <param name="enableHttpCompression">if set to <c>true</c> [enable HTTP compression].</param>
/// <returns>HttpClient.</returns> /// <returns>HttpClient.</returns>
/// <exception cref="ArgumentNullException">host</exception> private HttpClient GetHttpClient(string url)
private HttpClientInfo GetHttpClient(string host, bool enableHttpCompression)
{ {
if (string.IsNullOrEmpty(host)) var key = GetHostFromUrl(url);
{
throw new ArgumentNullException(nameof(host));
}
var key = host + enableHttpCompression;
if (!_httpClients.TryGetValue(key, out var client)) if (!_httpClients.TryGetValue(key, out var client))
{ {
client = new HttpClientInfo(); client = new HttpClient()
{
BaseAddress = new Uri(url)
};
_httpClients.TryAdd(key, client); _httpClients.TryAdd(key, client);
} }
@@ -102,119 +80,84 @@ namespace Emby.Server.Implementations.HttpClientManager
return client; return client;
} }
private WebRequest GetRequest(HttpRequestOptions options, string method) private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method)
{ {
string url = options.Url; string url = options.Url;
var uriAddress = new Uri(url); var uriAddress = new Uri(url);
string userInfo = uriAddress.UserInfo; string userInfo = uriAddress.UserInfo;
if (!string.IsNullOrWhiteSpace(userInfo)) if (!string.IsNullOrWhiteSpace(userInfo))
{ {
_logger.LogInformation("Found userInfo in url: {0} ... url: {1}", userInfo, url); _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
url = url.Replace(userInfo + "@", string.Empty); url = url.Replace(userInfo + '@', string.Empty);
} }
var request = WebRequest.Create(url); var request = new HttpRequestMessage(method, url);
if (request is HttpWebRequest httpWebRequest) AddRequestHeaders(request, options);
switch (options.DecompressionMethod)
{ {
AddRequestHeaders(httpWebRequest, options); case CompressionMethod.Deflate | CompressionMethod.Gzip:
request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" });
if (options.EnableHttpCompression) break;
{ case CompressionMethod.Deflate:
httpWebRequest.AutomaticDecompression = DecompressionMethods.Deflate; request.Headers.Add(HeaderNames.AcceptEncoding, "deflate");
if (options.DecompressionMethod.HasValue break;
&& options.DecompressionMethod.Value == CompressionMethod.Gzip) case CompressionMethod.Gzip:
{ request.Headers.Add(HeaderNames.AcceptEncoding, "gzip");
httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip; break;
} default:
} break;
else
{
httpWebRequest.AutomaticDecompression = DecompressionMethods.None;
}
httpWebRequest.KeepAlive = options.EnableKeepAlive;
if (!string.IsNullOrEmpty(options.Host))
{
httpWebRequest.Host = options.Host;
}
if (!string.IsNullOrEmpty(options.Referer))
{
httpWebRequest.Referer = options.Referer;
}
} }
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); if (options.EnableKeepAlive)
{
request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
}
request.Method = method; //request.Headers.Add(HeaderNames.CacheControl, "no-cache");
request.Timeout = options.TimeoutMs;
/*
if (!string.IsNullOrWhiteSpace(userInfo)) if (!string.IsNullOrWhiteSpace(userInfo))
{ {
var parts = userInfo.Split(':'); var parts = userInfo.Split(':');
if (parts.Length == 2) if (parts.Length == 2)
{ {
request.Credentials = GetCredential(url, parts[0], parts[1]); request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]);
// TODO: .net core ??
request.PreAuthenticate = true;
} }
} }
*/
return request; return request;
} }
private static CredentialCache GetCredential(string url, string username, string password) private void AddRequestHeaders(HttpRequestMessage request, HttpRequestOptions options)
{
//ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;
var credentialCache = new CredentialCache();
credentialCache.Add(new Uri(url), "Basic", new NetworkCredential(username, password));
return credentialCache;
}
private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options)
{ {
var hasUserAgent = false; var hasUserAgent = false;
foreach (var header in options.RequestHeaders) foreach (var header in options.RequestHeaders)
{ {
if (string.Equals(header.Key, HeaderNames.Accept, StringComparison.OrdinalIgnoreCase)) if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))
{ {
request.Accept = header.Value;
}
else if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))
{
SetUserAgent(request, header.Value);
hasUserAgent = true; hasUserAgent = true;
} }
else
{ request.Headers.Add(header.Key, header.Value);
request.Headers.Set(header.Key, header.Value);
}
} }
if (!hasUserAgent && options.EnableDefaultUserAgent) if (!hasUserAgent && options.EnableDefaultUserAgent)
{ {
SetUserAgent(request, _defaultUserAgentFn()); request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn());
} }
} }
private static void SetUserAgent(HttpWebRequest request, string userAgent)
{
request.UserAgent = userAgent;
}
/// <summary> /// <summary>
/// Gets the response internal. /// Gets the response internal.
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
/// <returns>Task{HttpResponseInfo}.</returns> /// <returns>Task{HttpResponseInfo}.</returns>
public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options) public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
{ => SendAsync(options, HttpMethod.Get);
return SendAsync(options, "GET");
}
/// <summary> /// <summary>
/// Performs a GET request and returns the resulting stream /// Performs a GET request and returns the resulting stream
@@ -233,9 +176,16 @@ namespace Emby.Server.Implementations.HttpClientManager
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
/// <param name="httpMethod">The HTTP method.</param> /// <param name="httpMethod">The HTTP method.</param>
/// <returns>Task{HttpResponseInfo}.</returns> /// <returns>Task{HttpResponseInfo}.</returns>
/// <exception cref="HttpException"> public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
/// </exception> => SendAsync(options, new HttpMethod(httpMethod));
public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
/// <summary>
/// send as an asynchronous operation.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="httpMethod">The HTTP method.</param>
/// <returns>Task{HttpResponseInfo}.</returns>
public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod)
{ {
if (options.CacheMode == CacheMode.None) if (options.CacheMode == CacheMode.None)
{ {
@@ -294,186 +244,89 @@ namespace Emby.Server.Implementations.HttpClientManager
} }
} }
private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, string httpMethod) private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod)
{ {
ValidateParams(options); ValidateParams(options);
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression); var client = GetHttpClient(options.Url);
if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds) var httpWebRequest = GetRequestMessage(options, httpMethod);
if (options.RequestContentBytes != null
|| !string.IsNullOrEmpty(options.RequestContent)
|| httpMethod == HttpMethod.Post)
{ {
throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url)) if (options.RequestContentBytes != null)
{ {
IsTimedOut = true httpWebRequest.Content = new ByteArrayContent(options.RequestContentBytes);
};
}
var httpWebRequest = GetRequest(options, httpMethod);
if (options.RequestContentBytes != null ||
!string.IsNullOrEmpty(options.RequestContent) ||
string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase))
{
try
{
var bytes = options.RequestContentBytes ?? Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty);
var contentType = options.RequestContentType ?? "application/x-www-form-urlencoded";
if (options.AppendCharsetToMimeType)
{
contentType = contentType.TrimEnd(';') + "; charset=\"utf-8\"";
}
httpWebRequest.ContentType = contentType;
(await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length);
} }
catch (Exception ex) else if (options.RequestContent != null)
{ {
throw new HttpException(ex.Message) { IsTimedOut = true }; httpWebRequest.Content = new StringContent(
options.RequestContent,
null,
options.RequestContentType);
}
else
{
httpWebRequest.Content = new ByteArrayContent(Array.Empty<byte>());
} }
}
if (options.ResourcePool != null)
{
await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
}
if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds)
{
options.ResourcePool?.Release();
throw new HttpException($"Connection to {options.Url} timed out") { IsTimedOut = true };
} }
if (options.LogRequest) if (options.LogRequest)
{ {
if (options.LogRequestAsDebug) _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url);
{
_logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url);
}
else
{
_logger.LogInformation("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url);
}
} }
try options.CancellationToken.ThrowIfCancellationRequested();
if (!options.BufferContent)
{ {
var response = await client.SendAsync(httpWebRequest, HttpCompletionOption.ResponseHeadersRead, options.CancellationToken).ConfigureAwait(false);
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
if (!options.BufferContent) var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return new HttpResponseInfo(response.Headers, response.Content.Headers)
{ {
var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false); Content = stream,
StatusCode = response.StatusCode,
ContentType = response.Content.Headers.ContentType?.MediaType,
ContentLength = response.Content.Headers.ContentLength,
ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
};
}
var httpResponse = (HttpWebResponse)response; using (var response = await client.SendAsync(httpWebRequest, HttpCompletionOption.ResponseHeadersRead, options.CancellationToken).ConfigureAwait(false))
{
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
EnsureSuccessStatusCode(client, httpResponse, options); options.CancellationToken.ThrowIfCancellationRequested();
options.CancellationToken.ThrowIfCancellationRequested(); using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
return GetResponseInfo(httpResponse, httpResponse.GetResponseStream(), GetContentLength(httpResponse), httpResponse);
}
using (var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false))
{ {
var httpResponse = (HttpWebResponse)response; var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
memoryStream.Position = 0;
EnsureSuccessStatusCode(client, httpResponse, options); return new HttpResponseInfo(response.Headers, response.Content.Headers)
options.CancellationToken.ThrowIfCancellationRequested();
using (var stream = httpResponse.GetResponseStream())
{ {
var memoryStream = new MemoryStream(); Content = memoryStream,
await stream.CopyToAsync(memoryStream).ConfigureAwait(false); StatusCode = response.StatusCode,
ContentType = response.Content.Headers.ContentType?.MediaType,
memoryStream.Position = 0; ContentLength = memoryStream.Length,
ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
return GetResponseInfo(httpResponse, memoryStream, memoryStream.Length, null); };
}
} }
} }
catch (OperationCanceledException ex)
{
throw GetCancellationException(options, client, options.CancellationToken, ex);
}
catch (Exception ex)
{
throw GetException(ex, options, client);
}
finally
{
options.ResourcePool?.Release();
}
}
private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength, IDisposable disposable)
{
var responseInfo = new HttpResponseInfo(disposable)
{
Content = content,
StatusCode = httpResponse.StatusCode,
ContentType = httpResponse.ContentType,
ContentLength = contentLength,
ResponseUrl = httpResponse.ResponseUri.ToString()
};
if (httpResponse.Headers != null)
{
SetHeaders(httpResponse.Headers, responseInfo);
}
return responseInfo;
}
private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, string tempFile, long? contentLength)
{
var responseInfo = new HttpResponseInfo
{
TempFilePath = tempFile,
StatusCode = httpResponse.StatusCode,
ContentType = httpResponse.ContentType,
ContentLength = contentLength
};
if (httpResponse.Headers != null)
{
SetHeaders(httpResponse.Headers, responseInfo);
}
return responseInfo;
}
private static void SetHeaders(WebHeaderCollection headers, HttpResponseInfo responseInfo)
{
foreach (var key in headers.AllKeys)
{
responseInfo.Headers[key] = headers[key];
}
} }
public Task<HttpResponseInfo> Post(HttpRequestOptions options) public Task<HttpResponseInfo> Post(HttpRequestOptions options)
{ => SendAsync(options, HttpMethod.Post);
return SendAsync(options, "POST");
}
/// <summary>
/// Performs a POST request
/// </summary>
/// <param name="options">The options.</param>
/// <param name="postData">Params to add to the POST data.</param>
/// <returns>stream on success, null on failure</returns>
public async Task<Stream> Post(HttpRequestOptions options, Dictionary<string, string> postData)
{
options.SetPostData(postData);
var response = await Post(options).ConfigureAwait(false);
return response.Content;
}
/// <summary> /// <summary>
/// Downloads the contents of a given url into a temporary location /// Downloads the contents of a given url into a temporary location
@@ -483,7 +336,6 @@ namespace Emby.Server.Implementations.HttpClientManager
public async Task<string> GetTempFile(HttpRequestOptions options) public async Task<string> GetTempFile(HttpRequestOptions options)
{ {
var response = await GetTempFileResponse(options).ConfigureAwait(false); var response = await GetTempFileResponse(options).ConfigureAwait(false);
return response.TempFilePath; return response.TempFilePath;
} }
@@ -502,44 +354,28 @@ namespace Emby.Server.Implementations.HttpClientManager
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
var httpWebRequest = GetRequest(options, "GET"); var httpWebRequest = GetRequestMessage(options, HttpMethod.Get);
if (options.ResourcePool != null)
{
await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
}
options.Progress.Report(0); options.Progress.Report(0);
if (options.LogRequest) if (options.LogRequest)
{ {
if (options.LogRequestAsDebug) _logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
{
_logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
}
else
{
_logger.LogInformation("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
}
} }
var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression); var client = GetHttpClient(options.Url);
try try
{ {
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false)) using (var response = (await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false)))
{ {
var httpResponse = (HttpWebResponse)response; await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
EnsureSuccessStatusCode(client, httpResponse, options);
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
var contentLength = GetContentLength(httpResponse); using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (var stream = httpResponse.GetResponseStream())
using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
{ {
await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
@@ -547,35 +383,29 @@ namespace Emby.Server.Implementations.HttpClientManager
options.Progress.Report(100); options.Progress.Report(100);
return GetResponseInfo(httpResponse, tempFile, contentLength); var responseInfo = new HttpResponseInfo(response.Headers, response.Content.Headers)
{
TempFilePath = tempFile,
StatusCode = response.StatusCode,
ContentType = response.Content.Headers.ContentType?.MediaType,
ContentLength = response.Content.Headers.ContentLength
};
return responseInfo;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
DeleteTempFile(tempFile); if (File.Exists(tempFile))
throw GetException(ex, options, client); {
} File.Delete(tempFile);
finally }
{
options.ResourcePool?.Release(); throw GetException(ex, options);
} }
} }
private static long? GetContentLength(HttpWebResponse response) private Exception GetException(Exception ex, HttpRequestOptions options)
{
var length = response.ContentLength;
if (length == 0)
{
return null;
}
return length;
}
protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
private Exception GetException(Exception ex, HttpRequestOptions options, HttpClientInfo client)
{ {
if (ex is HttpException) if (ex is HttpException)
{ {
@@ -589,7 +419,7 @@ namespace Emby.Server.Implementations.HttpClientManager
{ {
if (options.LogErrors) if (options.LogErrors)
{ {
_logger.LogError(webException, "Error {status} getting response from {url}", webException.Status, options.Url); _logger.LogError(webException, "Error {Status} getting response from {Url}", webException.Status, options.Url);
} }
var exception = new HttpException(webException.Message, webException); var exception = new HttpException(webException.Message, webException);
@@ -599,11 +429,6 @@ namespace Emby.Server.Implementations.HttpClientManager
if (response != null) if (response != null)
{ {
exception.StatusCode = response.StatusCode; exception.StatusCode = response.StatusCode;
if ((int)response.StatusCode == 429)
{
client.LastTimeout = DateTime.UtcNow;
}
} }
} }
@@ -624,29 +449,17 @@ namespace Emby.Server.Implementations.HttpClientManager
if (operationCanceledException != null) if (operationCanceledException != null)
{ {
return GetCancellationException(options, client, options.CancellationToken, operationCanceledException); return GetCancellationException(options, options.CancellationToken, operationCanceledException);
} }
if (options.LogErrors) if (options.LogErrors)
{ {
_logger.LogError(ex, "Error getting response from {url}", options.Url); _logger.LogError(ex, "Error getting response from {Url}", options.Url);
} }
return ex; return ex;
} }
private void DeleteTempFile(string file)
{
try
{
_fileSystem.DeleteFile(file);
}
catch (IOException)
{
// Might not have been created at all. No need to worry.
}
}
private void ValidateParams(HttpRequestOptions options) private void ValidateParams(HttpRequestOptions options)
{ {
if (string.IsNullOrEmpty(options.Url)) if (string.IsNullOrEmpty(options.Url))
@@ -682,11 +495,10 @@ namespace Emby.Server.Implementations.HttpClientManager
/// Throws the cancellation exception. /// Throws the cancellation exception.
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="exception">The exception.</param> /// <param name="exception">The exception.</param>
/// <returns>Exception.</returns> /// <returns>Exception.</returns>
private Exception GetCancellationException(HttpRequestOptions options, HttpClientInfo client, CancellationToken cancellationToken, OperationCanceledException exception) private Exception GetCancellationException(HttpRequestOptions options, CancellationToken cancellationToken, OperationCanceledException exception)
{ {
// If the HttpClient's timeout is reached, it will cancel the Task internally // If the HttpClient's timeout is reached, it will cancel the Task internally
if (!cancellationToken.IsCancellationRequested) if (!cancellationToken.IsCancellationRequested)
@@ -698,8 +510,6 @@ namespace Emby.Server.Implementations.HttpClientManager
_logger.LogError(msg); _logger.LogError(msg);
} }
client.LastTimeout = DateTime.UtcNow;
// Throw an HttpException so that the caller doesn't think it was cancelled by user code // Throw an HttpException so that the caller doesn't think it was cancelled by user code
return new HttpException(msg, exception) return new HttpException(msg, exception)
{ {
@@ -710,91 +520,20 @@ namespace Emby.Server.Implementations.HttpClientManager
return exception; return exception;
} }
private void EnsureSuccessStatusCode(HttpClientInfo client, HttpWebResponse response, HttpRequestOptions options) private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
{ {
var statusCode = response.StatusCode; if (response.IsSuccessStatusCode)
var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299;
if (isSuccessful)
{ {
return; return;
} }
if (options.LogErrorResponseBody) var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
{ _logger.LogError("HTTP request failed with message: {Message}", msg);
try
{
using (var stream = response.GetResponseStream())
{
if (stream != null)
{
using (var reader = new StreamReader(stream))
{
var msg = reader.ReadToEnd();
_logger.LogError(msg); throw new HttpException(response.ReasonPhrase)
}
}
}
}
catch
{
}
}
throw new HttpException(response.StatusDescription)
{ {
StatusCode = response.StatusCode StatusCode = response.StatusCode
}; };
} }
private static Task<WebResponse> GetResponseAsync(WebRequest request, TimeSpan timeout)
{
var taskCompletion = new TaskCompletionSource<WebResponse>();
var asyncTask = Task.Factory.FromAsync(request.BeginGetResponse, request.EndGetResponse, null);
ThreadPool.RegisterWaitForSingleObject((asyncTask as IAsyncResult).AsyncWaitHandle, TimeoutCallback, request, timeout, true);
var callback = new TaskCallback { taskCompletion = taskCompletion };
asyncTask.ContinueWith(callback.OnSuccess, TaskContinuationOptions.NotOnFaulted);
// Handle errors
asyncTask.ContinueWith(callback.OnError, TaskContinuationOptions.OnlyOnFaulted);
return taskCompletion.Task;
}
private static void TimeoutCallback(object state, bool timedOut)
{
if (timedOut && state != null)
{
var request = (WebRequest)state;
request.Abort();
}
}
private class TaskCallback
{
public TaskCompletionSource<WebResponse> taskCompletion;
public void OnSuccess(Task<WebResponse> task)
{
taskCompletion.TrySetResult(task.Result);
}
public void OnError(Task<WebResponse> task)
{
if (task.Exception == null)
{
taskCompletion.TrySetException(Enumerable.Empty<Exception>());
}
else
{
taskCompletion.TrySetException(task.Exception);
}
}
}
} }
} }
@@ -67,6 +67,7 @@ namespace Emby.Server.Implementations.HttpServer
if (string.IsNullOrWhiteSpace(rangeHeader)) if (string.IsNullOrWhiteSpace(rangeHeader))
{ {
Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
StatusCode = HttpStatusCode.OK; StatusCode = HttpStatusCode.OK;
} }
else else
@@ -99,10 +100,13 @@ namespace Emby.Server.Implementations.HttpServer
RangeStart = requestedRange.Key; RangeStart = requestedRange.Key;
RangeLength = 1 + RangeEnd - RangeStart; RangeLength = 1 + RangeEnd - RangeStart;
// Content-Length is the length of what we're serving, not the original content
var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentLength] = lengthString;
var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
Headers[HeaderNames.ContentRange] = rangeString; Headers[HeaderNames.ContentRange] = rangeString;
Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Range: {2}", Path, RangeHeader, rangeString); Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
} }
/// <summary> /// <summary>
@@ -203,6 +203,7 @@ namespace Emby.Server.Implementations.HttpServer
case DirectoryNotFoundException _: case DirectoryNotFoundException _:
case FileNotFoundException _: case FileNotFoundException _:
case ResourceNotFoundException _: return 404; case ResourceNotFoundException _: return 404;
case MethodNotAllowedException _: return 405;
case RemoteServiceUnavailableException _: return 502; case RemoteServiceUnavailableException _: return 502;
default: return 500; default: return 500;
} }
@@ -637,6 +638,7 @@ namespace Emby.Server.Implementations.HttpServer
private static Task Write(IResponse response, string text) private static Task Write(IResponse response, string text)
{ {
var bOutput = Encoding.UTF8.GetBytes(text); var bOutput = Encoding.UTF8.GetBytes(text);
response.OriginalResponse.ContentLength = bOutput.Length;
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
} }
@@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer
content = Array.Empty<byte>(); content = Array.Empty<byte>();
} }
result = new StreamWriter(content, contentType); result = new StreamWriter(content, contentType, contentLength);
} }
else else
{ {
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer
bytes = Array.Empty<byte>(); bytes = Array.Empty<byte>();
} }
result = new StreamWriter(bytes, contentType); result = new StreamWriter(bytes, contentType, contentLength);
} }
else else
{ {
@@ -335,13 +335,13 @@ namespace Emby.Server.Implementations.HttpServer
if (isHeadRequest) if (isHeadRequest)
{ {
var result = new StreamWriter(Array.Empty<byte>(), contentType); var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
return result; return result;
} }
else else
{ {
var result = new StreamWriter(content, contentType); var result = new StreamWriter(content, contentType, contentLength);
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
return result; return result;
} }
@@ -581,6 +581,11 @@ namespace Emby.Server.Implementations.HttpServer
} }
else else
{ {
if (totalContentLength.HasValue)
{
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
}
if (isHeadRequest) if (isHeadRequest)
{ {
using (stream) using (stream)
@@ -624,7 +629,7 @@ namespace Emby.Server.Implementations.HttpServer
if (lastModifiedDate.HasValue) if (lastModifiedDate.HasValue)
{ {
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString(); responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
} }
} }
@@ -96,6 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
RangeStart = requestedRange.Key; RangeStart = requestedRange.Key;
RangeLength = 1 + RangeEnd - RangeStart; RangeLength = 1 + RangeEnd - RangeStart;
Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
if (RangeStart > 0 && SourceStream.CanSeek) if (RangeStart > 0 && SourceStream.CanSeek)
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer
public void FilterResponse(IRequest req, IResponse res, object dto) public void FilterResponse(IRequest req, IResponse res, object dto)
{ {
// Try to prevent compatibility view // Try to prevent compatibility view
res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
res.AddHeader("Access-Control-Allow-Origin", "*"); res.AddHeader("Access-Control-Allow-Origin", "*");
@@ -58,6 +58,7 @@ namespace Emby.Server.Implementations.HttpServer
if (length > 0) if (length > 0)
{ {
res.OriginalResponse.ContentLength = length;
res.SendChunked = false; res.SendChunked = false;
} }
} }
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -49,6 +50,13 @@ namespace Emby.Server.Implementations.HttpServer
SourceStream = source; SourceStream = source;
Headers["Content-Type"] = contentType;
if (source.CanSeek)
{
Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
}
Headers[HeaderNames.ContentType] = contentType; Headers[HeaderNames.ContentType] = contentType;
} }
@@ -57,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
/// <param name="source">The source.</param> /// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</param> /// <param name="contentType">Type of the content.</param>
public StreamWriter(byte[] source, string contentType) public StreamWriter(byte[] source, string contentType, int contentLength)
{ {
if (string.IsNullOrEmpty(contentType)) if (string.IsNullOrEmpty(contentType))
{ {
@@ -66,6 +74,7 @@ namespace Emby.Server.Implementations.HttpServer
SourceBytes = source; SourceBytes = source;
Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentType] = contentType; Headers[HeaderNames.ContentType] = contentType;
} }
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.Library
public string Name => "Default"; public string Name => "Default";
public bool IsEnabled => true; public bool IsEnabled => true;
// This is dumb and an artifact of the backwards way auth providers were designed. // This is dumb and an artifact of the backwards way auth providers were designed.
// This version of authenticate was never meant to be called, but needs to be here for interface compat // This version of authenticate was never meant to be called, but needs to be here for interface compat
// Only the providers that don't provide local user support use this // Only the providers that don't provide local user support use this
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
// This is the verson that we need to use for local users. Because reasons. // This is the verson that we need to use for local users. Because reasons.
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
{ {
@@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
string hash = user.Password; string hash = user.Password;
user.Password = string.Format("$SHA1${0}", hash); user.Password = string.Format("$SHA1${0}", hash);
} }
if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
{ {
string hash = user.EasyPassword; string hash = user.EasyPassword;
@@ -165,6 +165,34 @@ namespace Emby.Server.Implementations.Library
return user.Password; return user.Password;
} }
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
{
ConvertPasswordFormat(user);
if (newPassword != null)
{
newPasswordHash = string.Format("$SHA1${0}", GetHashedString(user, newPassword));
}
if (string.IsNullOrWhiteSpace(newPasswordHash))
{
throw new ArgumentNullException(nameof(newPasswordHash));
}
user.EasyPassword = newPasswordHash;
}
public string GetEasyPasswordHash(User user)
{
// This should be removed in the future. This was added to let user login after
// Jellyfin 10.3.3 failed to save a well formatted PIN.
ConvertPasswordFormat(user);
return string.IsNullOrEmpty(user.EasyPassword)
? null
: (new PasswordHash(user.EasyPassword)).Hash;
}
public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
{ {
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net;
namespace Emby.Server.Implementations.Library
{
public class InvalidAuthProvider : IAuthenticationProvider
{
public string Name => "InvalidOrMissingAuthenticationProvider";
public bool IsEnabled => true;
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
{
throw new SecurityException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
}
public Task<bool> HasPassword(User user)
{
return Task.FromResult(true);
}
public Task ChangePassword(User user, string newPassword)
{
return Task.CompletedTask;
}
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
{
// Nothing here
}
public string GetPasswordHash(User user)
{
return string.Empty;
}
public string GetEasyPasswordHash(User user)
{
return string.Empty;
}
}
}
@@ -79,6 +79,8 @@ namespace Emby.Server.Implementations.Library
private IAuthenticationProvider[] _authenticationProviders; private IAuthenticationProvider[] _authenticationProviders;
private DefaultAuthenticationProvider _defaultAuthenticationProvider; private DefaultAuthenticationProvider _defaultAuthenticationProvider;
private InvalidAuthProvider _invalidAuthProvider;
private IPasswordResetProvider[] _passwordResetProviders; private IPasswordResetProvider[] _passwordResetProviders;
private DefaultPasswordResetProvider _defaultPasswordResetProvider; private DefaultPasswordResetProvider _defaultPasswordResetProvider;
@@ -141,6 +143,8 @@ namespace Emby.Server.Implementations.Library
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_passwordResetProviders = passwordResetProviders.ToArray(); _passwordResetProviders = passwordResetProviders.ToArray();
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); _defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
@@ -307,8 +311,7 @@ namespace Emby.Server.Implementations.Library
user = Users user = Users
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy; if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
if (hasNewUserPolicy != null)
{ {
var policy = hasNewUserPolicy.GetNewUserPolicy(); var policy = hasNewUserPolicy.GetNewUserPolicy();
UpdateUserPolicy(user, policy, true); UpdateUserPolicy(user, policy, true);
@@ -400,7 +403,9 @@ namespace Emby.Server.Implementations.Library
if (providers.Length == 0) if (providers.Length == 0)
{ {
providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider }; // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
_logger.LogWarning("User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", user.Name, user.Policy.AuthenticationProviderId);
providers = new IAuthenticationProvider[] { _invalidAuthProvider };
} }
return providers; return providers;
@@ -471,7 +476,7 @@ namespace Emby.Server.Implementations.Library
if (password == null) if (password == null)
{ {
// legacy // legacy
success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); success = string.Equals(GetAuthenticationProvider(user).GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
} }
else else
{ {
@@ -497,11 +502,11 @@ namespace Emby.Server.Implementations.Library
if (password == null) if (password == null)
{ {
// legacy // legacy
success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
} }
else else
{ {
success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
} }
} }
} }
@@ -546,13 +551,6 @@ namespace Emby.Server.Implementations.Library
} }
} }
private string GetLocalPasswordHash(User user)
{
return string.IsNullOrEmpty(user.EasyPassword)
? null
: user.EasyPassword;
}
/// <summary> /// <summary>
/// Loads the users from the repository /// Loads the users from the repository
/// </summary> /// </summary>
@@ -596,7 +594,7 @@ namespace Emby.Server.Implementations.Library
} }
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user)); bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user));
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
hasConfiguredEasyPassword : hasConfiguredEasyPassword :
@@ -884,17 +882,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(user)); throw new ArgumentNullException(nameof(user));
} }
if (newPassword != null) GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordHash);
{
newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword);
}
if (string.IsNullOrWhiteSpace(newPasswordHash))
{
throw new ArgumentNullException(nameof(newPasswordHash));
}
user.EasyPassword = newPasswordHash;
UpdateUser(user); UpdateUser(user);
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
UserAgent = "Emby/3.0", UserAgent = "Emby/3.0",
// Shouldn't matter but may cause issues // Shouldn't matter but may cause issues
EnableHttpCompression = false DecompressionMethod = CompressionMethod.None
}; };
using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false)) using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))
@@ -96,8 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Url = ApiUrl + "/schedules", Url = ApiUrl + "/schedules",
UserAgent = UserAgent, UserAgent = UserAgent,
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
// The data can be large so give it some extra time
TimeoutMs = 60000,
LogErrorResponseBody = true, LogErrorResponseBody = true,
RequestContent = requestString RequestContent = requestString
}; };
@@ -115,9 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Url = ApiUrl + "/programs", Url = ApiUrl + "/programs",
UserAgent = UserAgent, UserAgent = UserAgent,
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
LogErrorResponseBody = true, LogErrorResponseBody = true
// The data can be large so give it some extra time
TimeoutMs = 60000
}; };
httpOptions.RequestHeaders["token"] = token; httpOptions.RequestHeaders["token"] = token;
@@ -483,8 +479,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
RequestContent = imageIdString, RequestContent = imageIdString,
LogErrorResponseBody = true, LogErrorResponseBody = true,
// The data can be large so give it some extra time
TimeoutMs = 60000
}; };
try try
@@ -633,15 +627,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
ListingsProviderInfo providerInfo) ListingsProviderInfo providerInfo)
{ {
// Schedules direct requires that the client support compression and will return a 400 response without it // Schedules direct requires that the client support compression and will return a 400 response without it
options.EnableHttpCompression = true; options.DecompressionMethod = CompressionMethod.Deflate;
// On windows 7 under .net core, this header is not getting added
#if NETSTANDARD2_0
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate";
}
#endif
try try
{ {
@@ -671,15 +657,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
ListingsProviderInfo providerInfo) ListingsProviderInfo providerInfo)
{ {
// Schedules direct requires that the client support compression and will return a 400 response without it // Schedules direct requires that the client support compression and will return a 400 response without it
options.EnableHttpCompression = true; options.DecompressionMethod = CompressionMethod.Deflate;
// On windows 7 under .net core, this header is not getting added
#if NETSTANDARD2_0
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
options.RequestHeaders[HeaderNames.AcceptEncoding] = "deflate";
}
#endif
try try
{ {
@@ -871,8 +849,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
UserAgent = UserAgent, UserAgent = UserAgent,
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
LogErrorResponseBody = true, LogErrorResponseBody = true,
// The data can be large so give it some extra time
TimeoutMs = 60000
}; };
httpOptions.RequestHeaders["token"] = token; httpOptions.RequestHeaders["token"] = token;
@@ -2,14 +2,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.XmlTv.Classes; using Emby.XmlTv.Classes;
using Emby.XmlTv.Entities; using Emby.XmlTv.Entities;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@@ -27,7 +28,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IZipClient _zipClient; private readonly IZipClient _zipClient;
public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger, IFileSystem fileSystem, IZipClient zipClient) public XmlTvListingsProvider(
IServerConfigurationManager config,
IHttpClient httpClient,
ILogger logger,
IFileSystem fileSystem,
IZipClient zipClient)
{ {
_config = config; _config = config;
_httpClient = httpClient; _httpClient = httpClient;
@@ -52,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private async Task<string> GetXml(string path, CancellationToken cancellationToken) private async Task<string> GetXml(string path, CancellationToken cancellationToken)
{ {
_logger.LogInformation("xmltv path: {path}", path); _logger.LogInformation("xmltv path: {Path}", path);
if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{ {
@@ -66,26 +72,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return UnzipIfNeeded(path, cacheFile); return UnzipIfNeeded(path, cacheFile);
} }
_logger.LogInformation("Downloading xmltv listings from {path}", path); _logger.LogInformation("Downloading xmltv listings from {Path}", path);
string tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
{
CancellationToken = cancellationToken,
Url = path,
Progress = new SimpleProgress<double>(),
DecompressionMethod = CompressionMethod.Gzip,
// It's going to come back gzipped regardless of this value
// So we need to make sure the decompression method is set to gzip
EnableHttpCompression = true,
UserAgent = "Emby/3.0"
}).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
File.Copy(tempFile, cacheFile, true); using (var res = await _httpClient.SendAsync(
new HttpRequestOptions
{
CancellationToken = cancellationToken,
Url = path,
DecompressionMethod = CompressionMethod.Gzip,
},
HttpMethod.Get).ConfigureAwait(false))
using (var stream = res.Content)
using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
{
if (res.ContentHeaders.ContentEncoding.Contains("gzip"))
{
using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
{
await gzStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
else
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
return UnzipIfNeeded(path, cacheFile); return UnzipIfNeeded(path, cacheFile);
} }
@@ -103,7 +116,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error extracting from gz file {file}", file); _logger.LogError(ex, "Error extracting from gz file {File}", file);
} }
try try
@@ -113,7 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error extracting from zip file {file}", file); _logger.LogError(ex, "Error extracting from zip file {File}", file);
} }
} }
@@ -161,20 +174,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
throw new ArgumentNullException(nameof(channelId)); throw new ArgumentNullException(nameof(channelId));
} }
/* _logger.LogDebug("Getting xmltv programs for channel {Id}", channelId);
if (!await EmbyTV.EmbyTVRegistration.Instance.EnableXmlTv().ConfigureAwait(false))
{
var length = endDateUtc - startDateUtc;
if (length.TotalDays > 1)
{
endDateUtc = startDateUtc.AddDays(1);
}
}*/
_logger.LogDebug("Getting xmltv programs for channel {id}", channelId);
string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Opening XmlTvReader for {path}", path); _logger.LogDebug("Opening XmlTvReader for {Path}", path);
var reader = new XmlTvReader(path, GetLanguage(info)); var reader = new XmlTvReader(path, GetLanguage(info));
return reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken) return reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken)
@@ -267,7 +270,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{ {
// In theory this should never be called because there is always only one lineup // In theory this should never be called because there is always only one lineup
string path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false); string path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false);
_logger.LogDebug("Opening XmlTvReader for {path}", path); _logger.LogDebug("Opening XmlTvReader for {Path}", path);
var reader = new XmlTvReader(path, GetLanguage(info)); var reader = new XmlTvReader(path, GetLanguage(info));
IEnumerable<XmlTvChannel> results = reader.GetChannels(); IEnumerable<XmlTvChannel> results = reader.GetChannels();
@@ -279,7 +282,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{ {
// In theory this should never be called because there is always only one lineup // In theory this should never be called because there is always only one lineup
string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Opening XmlTvReader for {path}", path); _logger.LogDebug("Opening XmlTvReader for {Path}", path);
var reader = new XmlTvReader(path, GetLanguage(info)); var reader = new XmlTvReader(path, GetLanguage(info));
var results = reader.GetChannels(); var results = reader.GetChannels();
@@ -138,7 +138,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
Url = string.Format("{0}/discover.json", GetApiUrl(info)), Url = string.Format("{0}/discover.json", GetApiUrl(info)),
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(10).TotalMilliseconds),
BufferContent = false BufferContent = false
}, "GET").ConfigureAwait(false)) }, "GET").ConfigureAwait(false))
@@ -191,7 +190,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
Url = string.Format("{0}/tuners.html", GetApiUrl(info)), Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
BufferContent = false BufferContent = false
})) }))
{ {
@@ -47,13 +47,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
CancellationToken = CancellationToken.None, CancellationToken = CancellationToken.None,
BufferContent = false, BufferContent = false,
// Increase a little bit DecompressionMethod = CompressionMethod.None,
TimeoutMs = 30000,
EnableHttpCompression = false,
LogResponse = true,
LogResponseHeaders = true
}; };
foreach (var header in mediaSource.RequiredHttpHeaders) foreach (var header in mediaSource.RequiredHttpHeaders)
@@ -5,7 +5,7 @@
"Artists": "Umělci", "Artists": "Umělci",
"AuthenticationSucceededWithUserName": "{0} úspěšně ověřen", "AuthenticationSucceededWithUserName": "{0} úspěšně ověřen",
"Books": "Knihy", "Books": "Knihy",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", "CameraImageUploadedFrom": "Z {0} byla nahrána nová fotografie",
"Channels": "Kanály", "Channels": "Kanály",
"ChapterNameValue": "Kapitola {0}", "ChapterNameValue": "Kapitola {0}",
"Collections": "Kolekce", "Collections": "Kolekce",
@@ -16,14 +16,14 @@
"Folders": "Složky", "Folders": "Složky",
"Genres": "Žánry", "Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba", "HeaderAlbumArtists": "Umělci alba",
"HeaderCameraUploads": "Camera Uploads", "HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovat ve sledování", "HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba", "HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení umělci", "HeaderFavoriteArtists": "Oblíbení interpreti",
"HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbené písně", "HeaderFavoriteSongs": "Oblíbená hudba",
"HeaderLiveTV": "Živá TV", "HeaderLiveTV": "Live TV",
"HeaderNextUp": "Nadcházející", "HeaderNextUp": "Nadcházející",
"HeaderRecordingGroups": "Skupiny nahrávek", "HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domáci videa", "HomeVideos": "Domáci videa",
@@ -34,17 +34,17 @@
"LabelRunningTimeValue": "Délka média: {0}", "LabelRunningTimeValue": "Délka média: {0}",
"Latest": "Nejnovější", "Latest": "Nejnovější",
"MessageApplicationUpdated": "Jellyfin Server byl aktualizován", "MessageApplicationUpdated": "Jellyfin Server byl aktualizován",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageApplicationUpdatedTo": "Jellyfin server byl aktualizován na verzi {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizována", "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizována",
"MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována", "MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována",
"MixedContent": "Smíšený obsah", "MixedContent": "Smíšený obsah",
"Movies": "Filmy", "Movies": "Filmy",
"Music": "Hudba", "Music": "Hudba",
"MusicVideos": "Hudební klipy", "MusicVideos": "Hudební klipy",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "Instalace {0} selhala",
"NameSeasonNumber": "Sezóna {0}", "NameSeasonNumber": "Sezóna {0}",
"NameSeasonUnknown": "Neznámá sezóna", "NameSeasonUnknown": "Neznámá sezóna",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", "NewVersionIsAvailable": "Nová verze Jellyfin serveru je k dispozici ke stažení.",
"NotificationOptionApplicationUpdateAvailable": "Dostupná aktualizace aplikace", "NotificationOptionApplicationUpdateAvailable": "Dostupná aktualizace aplikace",
"NotificationOptionApplicationUpdateInstalled": "Aktualizace aplikace instalována", "NotificationOptionApplicationUpdateInstalled": "Aktualizace aplikace instalována",
"NotificationOptionAudioPlayback": "Přehrávání audia zahájeno", "NotificationOptionAudioPlayback": "Přehrávání audia zahájeno",
@@ -70,12 +70,12 @@
"ProviderValue": "Poskytl: {0}", "ProviderValue": "Poskytl: {0}",
"ScheduledTaskFailedWithName": "{0} selhalo", "ScheduledTaskFailedWithName": "{0} selhalo",
"ScheduledTaskStartedWithName": "{0} zahájeno", "ScheduledTaskStartedWithName": "{0} zahájeno",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "{0} vyžaduje restart",
"Shows": "Seriály", "Shows": "Seriály",
"Songs": "Skladby", "Songs": "Skladby",
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.", "StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
"SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}", "SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
"SubtitlesDownloadedForItem": "Staženy titulky pro {0}", "SubtitlesDownloadedForItem": "Staženy titulky pro {0}",
"Sync": "Synchronizace", "Sync": "Synchronizace",
"System": "Systém", "System": "Systém",
@@ -88,10 +88,10 @@
"UserOfflineFromDevice": "{0} se odpojil od {1}", "UserOfflineFromDevice": "{0} se odpojil od {1}",
"UserOnlineFromDevice": "{0} se připojil z {1}", "UserOnlineFromDevice": "{0} se připojil z {1}",
"UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}", "UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}",
"UserPolicyUpdatedWithName": "User policy has been updated for {0}", "UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány",
"UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}", "UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}",
"UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}", "UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueHasBeenAddedToLibrary": "{0} byl přidán do vaší knihovny médií",
"ValueSpecialEpisodeName": "Speciál - {0}", "ValueSpecialEpisodeName": "Speciál - {0}",
"VersionNumber": "Verze {0}" "VersionNumber": "Verze {0}"
} }
@@ -61,8 +61,8 @@
"NotificationOptionUserLockedOut": "Bruger låst ude", "NotificationOptionUserLockedOut": "Bruger låst ude",
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt", "NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
"NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet", "NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet",
"Photos": "Fotos", "Photos": "Fotoer",
"Playlists": "Spillelister", "Playlists": "Afspilningslister",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} blev installeret", "PluginInstalledWithName": "{0} blev installeret",
"PluginUninstalledWithName": "{0} blev afinstalleret", "PluginUninstalledWithName": "{0} blev afinstalleret",
@@ -16,7 +16,7 @@
"Folders": "Φάκελοι", "Folders": "Φάκελοι",
"Genres": "Είδη", "Genres": "Είδη",
"HeaderAlbumArtists": "Άλμπουμ Καλλιτεχνών", "HeaderAlbumArtists": "Άλμπουμ Καλλιτεχνών",
"HeaderCameraUploads": "Camera Uploads", "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
"HeaderContinueWatching": "Συνεχίστε να παρακολουθείτε", "HeaderContinueWatching": "Συνεχίστε να παρακολουθείτε",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ", "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες", "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
@@ -34,7 +34,7 @@
"LabelRunningTimeValue": "Διάρκεια: {0}", "LabelRunningTimeValue": "Διάρκεια: {0}",
"Latest": "Πρόσφατα", "Latest": "Πρόσφατα",
"MessageApplicationUpdated": "Ο Jellyfin Server έχει ενημερωθεί", "MessageApplicationUpdated": "Ο Jellyfin Server έχει ενημερωθεί",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageApplicationUpdatedTo": "Ο server Jellyfin αναβαθμίστηκε σε έκδοση {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του server έχει ενημερωθεί", "MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του server έχει ενημερωθεί",
"MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του server έχει ενημερωθεί", "MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του server έχει ενημερωθεί",
"MixedContent": "Ανάμεικτο Περιεχόμενο", "MixedContent": "Ανάμεικτο Περιεχόμενο",
@@ -49,7 +49,7 @@
"NotificationOptionApplicationUpdateInstalled": "Η ενημέρωση εφαρμογής εγκαταστάθηκε", "NotificationOptionApplicationUpdateInstalled": "Η ενημέρωση εφαρμογής εγκαταστάθηκε",
"NotificationOptionAudioPlayback": "Η αναπαραγωγή ήχου ξεκίνησε", "NotificationOptionAudioPlayback": "Η αναπαραγωγή ήχου ξεκίνησε",
"NotificationOptionAudioPlaybackStopped": "Η αναπαραγωγή ήχου σταμάτησε", "NotificationOptionAudioPlaybackStopped": "Η αναπαραγωγή ήχου σταμάτησε",
"NotificationOptionCameraImageUploaded": "Camera image uploaded", "NotificationOptionCameraImageUploaded": "Μεταφορτώθηκε φωτογραφία απο κάμερα",
"NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης", "NotificationOptionInstallationFailed": "Αποτυχία εγκατάστασης",
"NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο", "NotificationOptionNewLibraryContent": "Προστέθηκε νέο περιεχόμενο",
"NotificationOptionPluginError": "Αποτυχία του plugin", "NotificationOptionPluginError": "Αποτυχία του plugin",
@@ -75,7 +75,7 @@
"Songs": "Τραγούδια", "Songs": "Τραγούδια",
"StartupEmbyServerIsLoading": "Ο Jellyfin Server φορτώνει. Παρακαλώ δοκιμάστε σε λίγο.", "StartupEmbyServerIsLoading": "Ο Jellyfin Server φορτώνει. Παρακαλώ δοκιμάστε σε λίγο.",
"SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}", "SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
"SubtitlesDownloadedForItem": "Οι υπότιτλοι κατέβηκαν για {0}", "SubtitlesDownloadedForItem": "Οι υπότιτλοι κατέβηκαν για {0}",
"Sync": "Συγχρονισμός", "Sync": "Συγχρονισμός",
"System": "Σύστημα", "System": "Σύστημα",
@@ -1,97 +1,97 @@
{ {
"Albums": "Albums", "Albums": "Albom",
"AppDeviceValues": "App: {0}, Device: {1}", "AppDeviceValues": "App: {0}, Grät: {1}",
"Application": "Application", "Application": "Aawändig",
"Artists": "Artists", "Artists": "Könstler",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated", "AuthenticationSucceededWithUserName": "{0} het sech aagmäudet",
"Books": "Büecher", "Books": "Büecher",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", "CameraImageUploadedFrom": "Es nöis Foti esch ufeglade worde vo {0}",
"Channels": "Channels", "Channels": "Kanäu",
"ChapterNameValue": "Chapter {0}", "ChapterNameValue": "Kapitu {0}",
"Collections": "Collections", "Collections": "Sammlige",
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} esch offline gange",
"DeviceOnlineWithName": "{0} is connected", "DeviceOnlineWithName": "{0} esch online cho",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}", "FailedLoginAttemptWithUserName": "Fäugschlagne Aamäudeversuech vo {0}",
"Favorites": "Favorites", "Favorites": "Favorite",
"Folders": "Folders", "Folders": "Ordner",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Albuminterprete", "HeaderAlbumArtists": "Albom-Könstler",
"HeaderCameraUploads": "Camera Uploads", "HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "Wiiterluege", "HeaderContinueWatching": "Wiiterluege",
"HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteAlbums": "Lieblingsalbe",
"HeaderFavoriteArtists": "Besti Interpret", "HeaderFavoriteArtists": "Lieblings-Interprete",
"HeaderFavoriteEpisodes": "Favorite Episodes", "HeaderFavoriteEpisodes": "Lieblingsepisode",
"HeaderFavoriteShows": "Favorite Shows", "HeaderFavoriteShows": "Lieblingsserie",
"HeaderFavoriteSongs": "Besti Lieder", "HeaderFavoriteSongs": "Lieblingslieder",
"HeaderLiveTV": "Live TV", "HeaderLiveTV": "Live-Färnseh",
"HeaderNextUp": "Next Up", "HeaderNextUp": "Als nächts",
"HeaderRecordingGroups": "Ufnahmegruppe", "HeaderRecordingGroups": "Ufnahmegruppe",
"HomeVideos": "Heimfilmli", "HomeVideos": "Heimfilmli",
"Inherit": "Hinzuefüege", "Inherit": "Hinzuefüege",
"ItemAddedWithName": "{0} was added to the library", "ItemAddedWithName": "{0} esch de Bibliothek dezuegfüegt worde",
"ItemRemovedWithName": "{0} was removed from the library", "ItemRemovedWithName": "{0} esch vo de Bibliothek entfärnt worde",
"LabelIpAddressValue": "Ip address: {0}", "LabelIpAddressValue": "IP-Adrässe: {0}",
"LabelRunningTimeValue": "Running time: {0}", "LabelRunningTimeValue": "Loufziit: {0}",
"Latest": "Letschte", "Latest": "Nöischti",
"MessageApplicationUpdated": "Jellyfin Server has been updated", "MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", "MessageApplicationUpdatedTo": "Jellyfin Server esch of Version {0} aktualisiert worde",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde",
"MessageServerConfigurationUpdated": "Server configuration has been updated", "MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde",
"MixedContent": "Gmischte Inhalt", "MixedContent": "Gmeschti Inhäut",
"Movies": "Movies", "Movies": "Film",
"Music": "Musig", "Music": "Musig",
"MusicVideos": "Musigfilm", "MusicVideos": "Musigvideos",
"NameInstallFailed": "{0} installation failed", "NameInstallFailed": "Installation vo {0} fäugschlage",
"NameSeasonNumber": "Season {0}", "NameSeasonNumber": "Staffle {0}",
"NameSeasonUnknown": "Season Unknown", "NameSeasonUnknown": "Staffle unbekannt",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", "NewVersionIsAvailable": "E nöi Version vo Jellyfin Server esch zom Download parat.",
"NotificationOptionApplicationUpdateAvailable": "Application update available", "NotificationOptionApplicationUpdateAvailable": "Aawändigsupdate verfüegbar",
"NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionApplicationUpdateInstalled": "Aawändigsupdate installiert",
"NotificationOptionAudioPlayback": "Audio playback started", "NotificationOptionAudioPlayback": "Audiowedergab gstartet",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped", "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
"NotificationOptionCameraImageUploaded": "Camera image uploaded", "NotificationOptionCameraImageUploaded": "Foti ueglade",
"NotificationOptionInstallationFailed": "Installation failure", "NotificationOptionInstallationFailed": "Installationsfäuer",
"NotificationOptionNewLibraryContent": "New content added", "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
"NotificationOptionPluginError": "Plugin failure", "NotificationOptionPluginError": "Plugin-Fäuer",
"NotificationOptionPluginInstalled": "Plugin installed", "NotificationOptionPluginInstalled": "Plugin installiert",
"NotificationOptionPluginUninstalled": "Plugin uninstalled", "NotificationOptionPluginUninstalled": "Plugin deinstalliert",
"NotificationOptionPluginUpdateInstalled": "Plugin update installed", "NotificationOptionPluginUpdateInstalled": "Pluginupdate installiert",
"NotificationOptionServerRestartRequired": "Server restart required", "NotificationOptionServerRestartRequired": "Serverneustart notwändig",
"NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionTaskFailed": "Planti Uufgab fäugschlage",
"NotificationOptionUserLockedOut": "User locked out", "NotificationOptionUserLockedOut": "Benotzer usgschlosse",
"NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlayback": "Videowedergab gstartet",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped", "NotificationOptionVideoPlaybackStopped": "Videowedergab gstoppt",
"Photos": "Fotis", "Photos": "Fotis",
"Playlists": "Abspielliste", "Playlists": "Wedergabeliste",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed", "PluginInstalledWithName": "{0} esch installiert worde",
"PluginUninstalledWithName": "{0} was uninstalled", "PluginUninstalledWithName": "{0} esch deinstalliert worde",
"PluginUpdatedWithName": "{0} was updated", "PluginUpdatedWithName": "{0} esch updated worde",
"ProviderValue": "Provider: {0}", "ProviderValue": "Aabieter: {0}",
"ScheduledTaskFailedWithName": "{0} failed", "ScheduledTaskFailedWithName": "{0} esch fäugschlage",
"ScheduledTaskStartedWithName": "{0} started", "ScheduledTaskStartedWithName": "{0} het gstartet",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "{0} mues nöi gstartet wärde",
"Shows": "Shows", "Shows": "Serie",
"Songs": "Songs", "Songs": "Lieder",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", "StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde",
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", "SubtitlesDownloadedForItem": "Ondertetle abeglade för {0}",
"Sync": "Sync", "Sync": "Synchronisation",
"System": "System", "System": "System",
"TvShows": "TV Shows", "TvShows": "Färnsehserie",
"User": "User", "User": "Benotzer",
"UserCreatedWithName": "User {0} has been created", "UserCreatedWithName": "Benotzer {0} esch erstöut worde",
"UserDeletedWithName": "User {0} has been deleted", "UserDeletedWithName": "Benotzer {0} esch glösche worde",
"UserDownloadingItemWithValues": "{0} is downloading {1}", "UserDownloadingItemWithValues": "{0} ladt {1} abe",
"UserLockedOutWithName": "User {0} has been locked out", "UserLockedOutWithName": "Benotzer {0} esch usgschlosse worde",
"UserOfflineFromDevice": "{0} has disconnected from {1}", "UserOfflineFromDevice": "{0} esch vo {1} trennt worde",
"UserOnlineFromDevice": "{0} is online from {1}", "UserOnlineFromDevice": "{0} esch online vo {1}",
"UserPasswordChangedWithName": "Password has been changed for user {0}", "UserPasswordChangedWithName": "S'Passwort för Benotzer {0} esch gänderet worde",
"UserPolicyUpdatedWithName": "User policy has been updated for {0}", "UserPolicyUpdatedWithName": "Benotzerrechtlinie för {0} esch aktualisiert worde",
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", "UserStartedPlayingItemWithValues": "{0} hed d'Wedergab vo {1} of {2} gstartet",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde",
"ValueSpecialEpisodeName": "Spezial - {0}", "ValueSpecialEpisodeName": "Extra - {0}",
"VersionNumber": "Version {0}" "VersionNumber": "Version {0}"
} }
@@ -5,7 +5,7 @@
"Artists": "Oryndaýshylar", "Artists": "Oryndaýshylar",
"AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy", "AuthenticationSucceededWithUserName": "{0} túpnusqalyq rastalýy sátti aıaqtaldy",
"Books": "Kitaptar", "Books": "Kitaptar",
"CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep alyndy", "CameraImageUploadedFrom": "{0} kamerasynan jańa sýret júktep salyndy",
"Channels": "Arnalar", "Channels": "Arnalar",
"ChapterNameValue": "{0}-sahna", "ChapterNameValue": "{0}-sahna",
"Collections": "Jıyntyqtar", "Collections": "Jıyntyqtar",
@@ -35,8 +35,8 @@
"Latest": "Eń keıingi", "Latest": "Eń keıingi",
"MessageApplicationUpdated": "Jellyfin Serveri jańartyldy", "MessageApplicationUpdated": "Jellyfin Serveri jańartyldy",
"MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy", "MessageApplicationUpdatedTo": "Jellyfin Serveri {0} nusqasyna jańartyldy",
"MessageNamedServerConfigurationUpdatedWithValue": "Server teńsheliminiń {0} bólimi jańartyldy", "MessageNamedServerConfigurationUpdatedWithValue": "Server konfıgýrasýasynyń {0} bólimi jańartyldy",
"MessageServerConfigurationUpdated": "Server teńshelimi jańartyldy", "MessageServerConfigurationUpdated": "Server konfıgýrasıasy jańartyldy",
"MixedContent": "Aralas mazmun", "MixedContent": "Aralas mazmun",
"Movies": "Fılmder", "Movies": "Fılmder",
"Music": "Mýzyka", "Music": "Mýzyka",
@@ -49,7 +49,7 @@
"NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy", "NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy",
"NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy", "NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy",
"NotificationOptionAudioPlaybackStopped": "Dybys oınatýy toqtatyldy", "NotificationOptionAudioPlaybackStopped": "Dybys oınatýy toqtatyldy",
"NotificationOptionCameraImageUploaded": "Kameradan fotosýret keri qotarylǵan", "NotificationOptionCameraImageUploaded": "Kameradan fotosýret júktep salynǵan",
"NotificationOptionInstallationFailed": "Ornatý sátsizdigi", "NotificationOptionInstallationFailed": "Ornatý sátsizdigi",
"NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen", "NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen",
"NotificationOptionPluginError": "Plagın sátsizdigi", "NotificationOptionPluginError": "Plagın sátsizdigi",
@@ -9,7 +9,7 @@
"Channels": "Kanali", "Channels": "Kanali",
"ChapterNameValue": "Poglavje {0}", "ChapterNameValue": "Poglavje {0}",
"Collections": "Zbirke", "Collections": "Zbirke",
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
"Favorites": "Priljubljeni", "Favorites": "Priljubljeni",
@@ -33,9 +33,9 @@
"LabelIpAddressValue": "IP naslov: {0}", "LabelIpAddressValue": "IP naslov: {0}",
"LabelRunningTimeValue": "Čas trajanja: {0}", "LabelRunningTimeValue": "Čas trajanja: {0}",
"Latest": "Najnovejše", "Latest": "Najnovejše",
"MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen", "MessageApplicationUpdated": "Jellyfin Server je bil posodobljen",
"MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}", "MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen",
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene", "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
"MixedContent": "Razne vsebine", "MixedContent": "Razne vsebine",
"Movies": "Filmi", "Movies": "Filmi",
@@ -57,41 +57,41 @@
"NotificationOptionPluginUninstalled": "Dodatek odstranjen", "NotificationOptionPluginUninstalled": "Dodatek odstranjen",
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena", "NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika", "NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
"NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionTaskFailed": "Razporejena naloga neuspešna",
"NotificationOptionUserLockedOut": "User locked out", "NotificationOptionUserLockedOut": "Uporabnik zaklenjen",
"NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped", "NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
"Photos": "Photos", "Photos": "Fotografije",
"Playlists": "Playlists", "Playlists": "Seznami predvajanja",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed", "PluginInstalledWithName": "{0} je bil nameščen",
"PluginUninstalledWithName": "{0} was uninstalled", "PluginUninstalledWithName": "{0} je bil odstranjen",
"PluginUpdatedWithName": "{0} was updated", "PluginUpdatedWithName": "{0} je bil posodobljen",
"ProviderValue": "Provider: {0}", "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} failed", "ScheduledTaskFailedWithName": "{0} ni uspelo",
"ScheduledTaskStartedWithName": "{0} started", "ScheduledTaskStartedWithName": "{0} začeto",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
"Shows": "Serije", "Shows": "Serije",
"Songs": "Songs", "Songs": "Pesmi",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", "StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", "SubtitlesDownloadedForItem": "Podnapisi preneseni za {0}",
"Sync": "Sync", "Sync": "Sinhroniziraj",
"System": "System", "System": "System",
"TvShows": "TV Shows", "TvShows": "TV serije",
"User": "User", "User": "User",
"UserCreatedWithName": "User {0} has been created", "UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
"UserDeletedWithName": "User {0} has been deleted", "UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
"UserDownloadingItemWithValues": "{0} is downloading {1}", "UserDownloadingItemWithValues": "{0} prenaša {1}",
"UserLockedOutWithName": "User {0} has been locked out", "UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen",
"UserOfflineFromDevice": "{0} has disconnected from {1}", "UserOfflineFromDevice": "{0} je prekinil povezavo z {1}",
"UserOnlineFromDevice": "{0} is online from {1}", "UserOnlineFromDevice": "{0} je aktiven iz {1}",
"UserPasswordChangedWithName": "Password has been changed for user {0}", "UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
"UserPolicyUpdatedWithName": "User policy has been updated for {0}", "UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", "UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
"ValueSpecialEpisodeName": "Special - {0}", "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}" "VersionNumber": "Version {0}"
} }
@@ -0,0 +1,93 @@
{
"Albums": "專輯",
"AppDeviceValues": "應用: {0}, 裝置: {1}",
"Application": "應用程式",
"Artists": "演出者",
"AuthenticationSucceededWithUserName": "{0} 成功授權",
"Books": "圖書",
"CameraImageUploadedFrom": "{0} 已經成功上傳一張相片",
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
"Collections": "合輯",
"DeviceOfflineWithName": "{0} 已經斷線",
"DeviceOnlineWithName": "{0} 已經連線",
"FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯演出者",
"HeaderCameraUploads": "相機上傳",
"HeaderContinueWatching": "繼續觀賞",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛演出者",
"HeaderFavoriteEpisodes": "最愛級數",
"HeaderFavoriteShows": "最愛節目",
"HeaderFavoriteSongs": "最愛歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "接下來",
"HomeVideos": "自製影片",
"ItemAddedWithName": "{0} 已新增至媒體庫",
"ItemRemovedWithName": "{0} 已從媒體庫移除",
"LabelIpAddressValue": "IP 位置: {0}",
"LabelRunningTimeValue": "運行時間: {0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin Server 已經更新",
"MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已經更新",
"MessageServerConfigurationUpdated": "伺服器設定已經更新",
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
"MusicVideos": "音樂MV",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
"NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
"NotificationOptionAudioPlayback": "音樂開始播放",
"NotificationOptionAudioPlaybackStopped": "音樂停止播放",
"NotificationOptionCameraImageUploaded": "相機相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已新增新內容",
"NotificationOptionPluginError": "外掛失敗",
"NotificationOptionPluginInstalled": "外掛已安裝",
"NotificationOptionPluginUninstalled": "外掛已移除",
"NotificationOptionPluginUpdateInstalled": "已更新外掛",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
"NotificationOptionTaskFailed": "排程任務失敗",
"NotificationOptionUserLockedOut": "使用者已鎖定",
"NotificationOptionVideoPlayback": "影片開始播放",
"NotificationOptionVideoPlaybackStopped": "影片停止播放",
"Photos": "相片",
"Playlists": "播放清單",
"Plugin": "外掛",
"PluginInstalledWithName": "{0} 已安裝",
"PluginUninstalledWithName": "{0} 已移除",
"PluginUpdatedWithName": "{0} 已更新",
"ProviderValue": "提供商: {0}",
"ScheduledTaskFailedWithName": "{0} 已失敗",
"ScheduledTaskStartedWithName": "{0} 已開始",
"ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
"Shows": "節目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin Server正在啟動,請稍後再試一次。",
"SubtitlesDownloadedForItem": "已為 {0} 下載字幕",
"Sync": "同步",
"System": "系統",
"TvShows": "電視節目",
"User": "使用者",
"UserCreatedWithName": "使用者 {0} 已建立",
"UserDeletedWithName": "使用者 {0} 已移除",
"UserDownloadingItemWithValues": "{0} 正在下載 {1}",
"UserLockedOutWithName": "使用者 {0} 已鎖定",
"UserOfflineFromDevice": "{0} 已從 {1} 斷線",
"UserOnlineFromDevice": "{0} 已連線,來自 {1}",
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
"UserPolicyUpdatedWithName": "使用者條約已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已新增至您的媒體庫",
"ValueSpecialEpisodeName": "特典 - {0}",
"VersionNumber": "版本 {0}"
}
@@ -43,6 +43,11 @@ namespace Emby.Server.Implementations.Services
{ {
var contentLength = bytesResponse.Length; var contentLength = bytesResponse.Length;
if (response != null)
{
response.OriginalResponse.ContentLength = contentLength;
}
if (contentLength > 0) if (contentLength > 0)
{ {
await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Text; using System.Text;
@@ -20,6 +21,8 @@ namespace Emby.Server.Implementations.Services
{ {
response.StatusCode = (int)HttpStatusCode.NoContent; response.StatusCode = (int)HttpStatusCode.NoContent;
} }
response.OriginalResponse.ContentLength = 0;
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -39,11 +42,6 @@ namespace Emby.Server.Implementations.Services
response.StatusCode = httpResult.Status; response.StatusCode = httpResult.Status;
response.StatusDescription = httpResult.StatusCode.ToString(); response.StatusDescription = httpResult.StatusCode.ToString();
//if (string.IsNullOrEmpty(httpResult.ContentType))
//{
// httpResult.ContentType = defaultContentType;
//}
//response.ContentType = httpResult.ContentType;
} }
var responseOptions = result as IHasHeaders; var responseOptions = result as IHasHeaders;
@@ -53,6 +51,7 @@ namespace Emby.Server.Implementations.Services
{ {
if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
{ {
response.OriginalResponse.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
continue; continue;
} }
@@ -72,52 +71,37 @@ namespace Emby.Server.Implementations.Services
response.ContentType += "; charset=utf-8"; response.ContentType += "; charset=utf-8";
} }
var asyncStreamWriter = result as IAsyncStreamWriter; switch (result)
if (asyncStreamWriter != null)
{ {
return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken); case IAsyncStreamWriter asyncStreamWriter:
} return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken);
case IStreamWriter streamWriter:
streamWriter.WriteTo(response.OutputStream);
return Task.CompletedTask;
case FileWriter fileWriter:
return fileWriter.WriteToAsync(response, cancellationToken);
case Stream stream:
return CopyStream(stream, response.OutputStream);
case byte[] bytes:
response.ContentType = "application/octet-stream";
response.OriginalResponse.ContentLength = bytes.Length;
var streamWriter = result as IStreamWriter; if (bytes.Length > 0)
if (streamWriter != null) {
{ return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
streamWriter.WriteTo(response.OutputStream); }
return Task.CompletedTask;
}
var fileWriter = result as FileWriter; return Task.CompletedTask;
if (fileWriter != null) case string responseText:
{ var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
return fileWriter.WriteToAsync(response, cancellationToken); response.OriginalResponse.ContentLength = responseTextAsBytes.Length;
}
var stream = result as Stream; if (responseTextAsBytes.Length > 0)
if (stream != null) {
{ return response.OutputStream.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
return CopyStream(stream, response.OutputStream); }
}
var bytes = result as byte[]; return Task.CompletedTask;
if (bytes != null)
{
response.ContentType = "application/octet-stream";
if (bytes.Length > 0)
{
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
}
return Task.CompletedTask;
}
var responseText = result as string;
if (responseText != null)
{
bytes = Encoding.UTF8.GetBytes(responseText);
if (bytes.Length > 0)
{
return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
}
return Task.CompletedTask;
} }
return WriteObject(request, result, response); return WriteObject(request, result, response);
@@ -143,14 +127,13 @@ namespace Emby.Server.Implementations.Services
ms.Position = 0; ms.Position = 0;
var contentLength = ms.Length; var contentLength = ms.Length;
response.OriginalResponse.ContentLength = contentLength;
if (contentLength > 0) if (contentLength > 0)
{ {
await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false); await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false);
} }
} }
//serializer(result, outputStream);
} }
} }
} }
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services
string propertyName = pair.Key; string propertyName = pair.Key;
string propertyTextValue = pair.Value; string propertyTextValue = pair.Value;
if (string.IsNullOrEmpty(propertyTextValue) if (propertyTextValue == null
|| !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry) || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
|| propertySerializerEntry.PropertySetFn == null) || propertySerializerEntry.PropertySetFn == null)
{ {
@@ -41,8 +41,6 @@ namespace Emby.Server.Implementations.Udp
_socketFactory = socketFactory; _socketFactory = socketFactory;
AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message); AddMessageResponder("who is JellyfinServer?", true, RespondToV2Message);
AddMessageResponder("who is EmbyServer?", true, RespondToV2Message);
AddMessageResponder("who is MediaBrowserServer_v2?", false, RespondToV2Message);
} }
private void AddMessageResponder(string message, bool isSubstring, Func<string, IpEndPointInfo, Encoding, CancellationToken, Task> responder) private void AddMessageResponder(string message, bool isSubstring, Func<string, IpEndPointInfo, Encoding, CancellationToken, Task> responder)
+5
View File
@@ -185,6 +185,11 @@ namespace Jellyfin.Drawing.Skia
public ImageDimensions GetImageSize(string path) public ImageDimensions GetImageSize(string path)
{ {
if (!File.Exists(path))
{
throw new FileNotFoundException("File not found", path);
}
using (var s = new SKFileStream(path)) using (var s = new SKFileStream(path))
using (var codec = SKCodec.Create(s)) using (var codec = SKCodec.Create(s))
{ {
+12
View File
@@ -118,8 +118,20 @@ namespace Jellyfin.Server
SQLitePCL.Batteries_V2.Init(); SQLitePCL.Batteries_V2.Init();
// Increase the max http request limit
// The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
// Disable the "Expect: 100-Continue" header by default
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
ServicePointManager.Expect100Continue = false;
// CA5359: Do Not Disable Certificate Validation
#pragma warning disable CA5359
// Allow all https requests // Allow all https requests
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
#pragma warning restore CA5359
var fileSystem = new ManagedFileSystem(_loggerFactory, appPaths); var fileSystem = new ManagedFileSystem(_loggerFactory, appPaths);
+12 -1
View File
@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Api.Movies; using MediaBrowser.Api.Movies;
@@ -828,7 +830,16 @@ namespace MediaBrowser.Api.Library
var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty); var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty);
if (!string.IsNullOrWhiteSpace(filename)) if (!string.IsNullOrWhiteSpace(filename))
{ {
headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\""; // Kestrel doesn't support non-ASCII characters in headers
if (Regex.IsMatch(filename, "[^[:ascii:]]"))
{
// Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2
headers[HeaderNames.ContentDisposition] = "attachment; filename*=UTF-8''" + WebUtility.UrlEncode(filename);
}
else
{
headers[HeaderNames.ContentDisposition] = "attachment; filename=\"" + filename + "\"";
}
} }
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -13,6 +14,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace MediaBrowser.Api.Playback.Progressive namespace MediaBrowser.Api.Playback.Progressive
@@ -279,10 +281,7 @@ namespace MediaBrowser.Api.Playback.Progressive
/// <returns>Task{System.Object}.</returns> /// <returns>Task{System.Object}.</returns>
private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource) private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
{ {
string useragent = null; state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent);
state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
var trySupportSeek = false;
var options = new HttpRequestOptions var options = new HttpRequestOptions
{ {
@@ -292,29 +291,14 @@ namespace MediaBrowser.Api.Playback.Progressive
CancellationToken = cancellationTokenSource.Token CancellationToken = cancellationTokenSource.Token
}; };
if (trySupportSeek)
{
if (!string.IsNullOrWhiteSpace(Request.QueryString[HeaderNames.Range]))
{
options.RequestHeaders[HeaderNames.Range] = Request.QueryString[HeaderNames.Range];
}
}
var response = await HttpClient.GetResponse(options).ConfigureAwait(false); var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
if (trySupportSeek) responseHeaders[HeaderNames.AcceptRanges] = "none";
// Seeing cases of -1 here
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
{ {
foreach (var name in new[] { HeaderNames.ContentRange, HeaderNames.AcceptRanges }) responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
{
var val = response.Headers[name];
if (!string.IsNullOrWhiteSpace(val))
{
responseHeaders[name] = val;
}
}
}
else
{
responseHeaders[HeaderNames.AcceptRanges] = "none";
} }
if (isHeadRequest) if (isHeadRequest)
@@ -356,10 +340,31 @@ namespace MediaBrowser.Api.Playback.Progressive
var contentType = state.GetMimeType(outputPath); var contentType = state.GetMimeType(outputPath);
// TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
if (contentLength.HasValue)
{
responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
}
// Headers only // Headers only
if (isHeadRequest) if (isHeadRequest)
{ {
return ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders); var streamResult = ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
if (streamResult is IHasHeaders hasHeaders)
{
if (contentLength.HasValue)
{
hasHeaders.Headers[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
}
else
{
hasHeaders.Headers.Remove(HeaderNames.ContentLength);
}
}
return streamResult;
} }
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath); var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
@@ -397,5 +402,22 @@ namespace MediaBrowser.Api.Playback.Progressive
transcodingLock.Release(); transcodingLock.Release();
} }
} }
/// <summary>
/// Gets the length of the estimated content.
/// </summary>
/// <param name="state">The state.</param>
/// <returns>System.Nullable{System.Int64}.</returns>
private long? GetEstimatedContentLength(StreamState state)
{
var totalBitrate = state.TotalOutputBitrate ?? 0;
if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
{
return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
}
return null;
}
} }
} }
+11 -1
View File
@@ -224,7 +224,17 @@ namespace MediaBrowser.Api.UserLibrary
request.IncludeItemTypes = "Playlist"; request.IncludeItemTypes = "Playlist";
} }
if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id)) bool isInEnabledFolder = user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id);
var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders)
{
if (user.Policy.EnabledFolders.Contains(collectionFolder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
{
isInEnabledFolder = true;
}
}
if (!(item is UserRootFolder) && !user.Policy.EnableAllFolders && !isInEnabledFolder)
{ {
Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Name, item.Name); Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Name, item.Name);
return new QueryResult<BaseItem> return new QueryResult<BaseItem>
+6 -1
View File
@@ -379,10 +379,15 @@ namespace MediaBrowser.Api
throw new ResourceNotFoundException("User not found"); throw new ResourceNotFoundException("User not found");
} }
if (!string.IsNullOrEmpty(request.Password) && string.IsNullOrEmpty(request.Pw))
{
throw new MethodNotAllowedException("Hashed-only passwords are not valid for this API.");
}
return Post(new AuthenticateUserByName return Post(new AuthenticateUserByName
{ {
Username = user.Name, Username = user.Name,
Password = request.Password, Password = null, // This should always be null
Pw = request.Pw Pw = request.Pw
}); });
} }
@@ -26,6 +26,30 @@ namespace MediaBrowser.Common.Extensions
} }
} }
/// <summary>
/// Class MethodNotAllowedException
/// </summary>
public class MethodNotAllowedException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
/// </summary>
public MethodNotAllowedException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
/// </summary>
/// <param name="message">The message.</param>
public MethodNotAllowedException(string message)
: base(message)
{
}
}
public class RemoteServiceUnavailableException : Exception public class RemoteServiceUnavailableException : Exception
{ {
public RemoteServiceUnavailableException() public RemoteServiceUnavailableException()
+17 -37
View File
@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@@ -17,7 +16,7 @@ namespace MediaBrowser.Common.Net
/// <value>The URL.</value> /// <value>The URL.</value>
public string Url { get; set; } public string Url { get; set; }
public CompressionMethod? DecompressionMethod { get; set; } public CompressionMethod DecompressionMethod { get; set; }
/// <summary> /// <summary>
/// Gets or sets the accept header. /// Gets or sets the accept header.
@@ -28,18 +27,13 @@ namespace MediaBrowser.Common.Net
get => GetHeaderValue(HeaderNames.Accept); get => GetHeaderValue(HeaderNames.Accept);
set => RequestHeaders[HeaderNames.Accept] = value; set => RequestHeaders[HeaderNames.Accept] = value;
} }
/// <summary> /// <summary>
/// Gets or sets the cancellation token. /// Gets or sets the cancellation token.
/// </summary> /// </summary>
/// <value>The cancellation token.</value> /// <value>The cancellation token.</value>
public CancellationToken CancellationToken { get; set; } public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Gets or sets the resource pool.
/// </summary>
/// <value>The resource pool.</value>
public SemaphoreSlim ResourcePool { get; set; }
/// <summary> /// <summary>
/// Gets or sets the user agent. /// Gets or sets the user agent.
/// </summary> /// </summary>
@@ -54,13 +48,21 @@ namespace MediaBrowser.Common.Net
/// Gets or sets the referrer. /// Gets or sets the referrer.
/// </summary> /// </summary>
/// <value>The referrer.</value> /// <value>The referrer.</value>
public string Referer { get; set; } public string Referer
{
get => GetHeaderValue(HeaderNames.Referer);
set => RequestHeaders[HeaderNames.Referer] = value;
}
/// <summary> /// <summary>
/// Gets or sets the host. /// Gets or sets the host.
/// </summary> /// </summary>
/// <value>The host.</value> /// <value>The host.</value>
public string Host { get; set; } public string Host
{
get => GetHeaderValue(HeaderNames.Host);
set => RequestHeaders[HeaderNames.Host] = value;
}
/// <summary> /// <summary>
/// Gets or sets the progress. /// Gets or sets the progress.
@@ -68,12 +70,6 @@ namespace MediaBrowser.Common.Net
/// <value>The progress.</value> /// <value>The progress.</value>
public IProgress<double> Progress { get; set; } public IProgress<double> Progress { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [enable HTTP compression].
/// </summary>
/// <value><c>true</c> if [enable HTTP compression]; otherwise, <c>false</c>.</value>
public bool EnableHttpCompression { get; set; }
public Dictionary<string, string> RequestHeaders { get; private set; } public Dictionary<string, string> RequestHeaders { get; private set; }
public string RequestContentType { get; set; } public string RequestContentType { get; set; }
@@ -86,8 +82,6 @@ namespace MediaBrowser.Common.Net
public bool LogRequest { get; set; } public bool LogRequest { get; set; }
public bool LogRequestAsDebug { get; set; } public bool LogRequestAsDebug { get; set; }
public bool LogErrors { get; set; } public bool LogErrors { get; set; }
public bool LogResponse { get; set; }
public bool LogResponseHeaders { get; set; }
public bool LogErrorResponseBody { get; set; } public bool LogErrorResponseBody { get; set; }
public bool EnableKeepAlive { get; set; } public bool EnableKeepAlive { get; set; }
@@ -95,12 +89,8 @@ namespace MediaBrowser.Common.Net
public CacheMode CacheMode { get; set; } public CacheMode CacheMode { get; set; }
public TimeSpan CacheLength { get; set; } public TimeSpan CacheLength { get; set; }
public int TimeoutMs { get; set; }
public bool EnableDefaultUserAgent { get; set; } public bool EnableDefaultUserAgent { get; set; }
public bool AppendCharsetToMimeType { get; set; }
public string DownloadFilePath { get; set; }
private string GetHeaderValue(string name) private string GetHeaderValue(string name)
{ {
RequestHeaders.TryGetValue(name, out var value); RequestHeaders.TryGetValue(name, out var value);
@@ -113,24 +103,12 @@ namespace MediaBrowser.Common.Net
/// </summary> /// </summary>
public HttpRequestOptions() public HttpRequestOptions()
{ {
EnableHttpCompression = true;
RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
LogRequest = true; LogRequest = true;
LogErrors = true; LogErrors = true;
CacheMode = CacheMode.None; CacheMode = CacheMode.None;
DecompressionMethod = CompressionMethod.Deflate;
TimeoutMs = 20000;
}
public void SetPostData(IDictionary<string, string> values)
{
var strings = values.Keys.Select(key => string.Format("{0}={1}", key, values[key]));
var postContent = string.Join("&", strings.ToArray());
RequestContent = postContent;
RequestContentType = "application/x-www-form-urlencoded";
} }
} }
@@ -140,9 +118,11 @@ namespace MediaBrowser.Common.Net
Unconditional = 1 Unconditional = 1
} }
[Flags]
public enum CompressionMethod public enum CompressionMethod
{ {
Deflate, None = 0b00000001,
Gzip Deflate = 0b00000010,
Gzip = 0b00000100
} }
} }
+15 -13
View File
@@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Net.Http.Headers;
namespace MediaBrowser.Common.Net namespace MediaBrowser.Common.Net
{ {
@@ -50,26 +50,28 @@ namespace MediaBrowser.Common.Net
/// Gets or sets the headers. /// Gets or sets the headers.
/// </summary> /// </summary>
/// <value>The headers.</value> /// <value>The headers.</value>
public Dictionary<string, string> Headers { get; set; } public HttpResponseHeaders Headers { get; set; }
private readonly IDisposable _disposable; /// <summary>
/// Gets or sets the content headers.
/// </summary>
/// <value>The content headers.</value>
public HttpContentHeaders ContentHeaders { get; set; }
public HttpResponseInfo(IDisposable disposable)
{
_disposable = disposable;
Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public HttpResponseInfo() public HttpResponseInfo()
{ {
Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public HttpResponseInfo(HttpResponseHeaders headers, HttpContentHeaders contentHeader)
{
Headers = headers;
ContentHeaders = contentHeader;
} }
public void Dispose() public void Dispose()
{ {
if (_disposable != null) // Only IDisposable for backwards compatibility
{
_disposable.Dispose();
}
} }
} }
} }
+11
View File
@@ -1,5 +1,6 @@
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Net.Http;
namespace MediaBrowser.Common.Net namespace MediaBrowser.Common.Net
{ {
@@ -23,6 +24,8 @@ namespace MediaBrowser.Common.Net
Task<Stream> Get(HttpRequestOptions options); Task<Stream> Get(HttpRequestOptions options);
/// <summary> /// <summary>
/// Warning: Deprecated function,
/// use 'Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead
/// Sends the asynchronous. /// Sends the asynchronous.
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
@@ -30,6 +33,14 @@ namespace MediaBrowser.Common.Net
/// <returns>Task{HttpResponseInfo}.</returns> /// <returns>Task{HttpResponseInfo}.</returns>
Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod); Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod);
/// <summary>
/// Sends the asynchronous.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="httpMethod">The HTTP method.</param>
/// <returns>Task{HttpResponseInfo}.</returns>
Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod);
/// <summary> /// <summary>
/// Posts the specified options. /// Posts the specified options.
/// </summary> /// </summary>
@@ -11,6 +11,9 @@ namespace MediaBrowser.Controller.Authentication
Task<ProviderAuthenticationResult> Authenticate(string username, string password); Task<ProviderAuthenticationResult> Authenticate(string username, string password);
Task<bool> HasPassword(User user); Task<bool> HasPassword(User user);
Task ChangePassword(User user, string newPassword); Task ChangePassword(User user, string newPassword);
void ChangeEasyPassword(User user, string newPassword, string newPasswordHash);
string GetPasswordHash(User user);
string GetEasyPasswordHash(User user);
} }
public interface IRequiresResolvedUser public interface IRequiresResolvedUser
+62 -29
View File
@@ -78,10 +78,25 @@ namespace MediaBrowser.Controller.Entities
/// <summary> /// <summary>
/// The trailer folder name /// The trailer folder name
/// </summary> /// </summary>
public static string TrailerFolderName = "trailers"; public const string TrailerFolderName = "trailers";
public static string ThemeSongsFolderName = "theme-music"; public const string ThemeSongsFolderName = "theme-music";
public static string ThemeSongFilename = "theme"; public const string ThemeSongFilename = "theme";
public static string ThemeVideosFolderName = "backdrops"; public const string ThemeVideosFolderName = "backdrops";
public const string ExtrasFolderName = "extras";
public const string BehindTheScenesFolderName = "behind the scenes";
public const string DeletedScenesFolderName = "deleted scenes";
public const string InterviewFolderName = "interviews";
public const string SceneFolderName = "scenes";
public const string SampleFolderName = "samples";
public static readonly string[] AllExtrasTypesFolderNames = {
ExtrasFolderName,
BehindTheScenesFolderName,
DeletedScenesFolderName,
InterviewFolderName,
SceneFolderName,
SampleFolderName
};
[IgnoreDataMember] [IgnoreDataMember]
public Guid[] ThemeSongIds { get; set; } public Guid[] ThemeSongIds { get; set; }
@@ -1276,16 +1291,15 @@ namespace MediaBrowser.Controller.Entities
.Select(item => .Select(item =>
{ {
// Try to retrieve it from the db. If we don't find it, use the resolved version // Try to retrieve it from the db. If we don't find it, use the resolved version
var dbItem = LibraryManager.GetItemById(item.Id) as Video;
if (dbItem != null) if (LibraryManager.GetItemById(item.Id) is Video dbItem)
{ {
item = dbItem; item = dbItem;
} }
else else
{ {
// item is new // item is new
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo; item.ExtraType = Model.Entities.ExtraType.ThemeVideo;
} }
return item; return item;
@@ -1296,33 +1310,38 @@ namespace MediaBrowser.Controller.Entities
protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{ {
var files = fileSystemChildren.Where(i => i.IsDirectory) var extras = new List<Video>();
.SelectMany(i => FileSystem.GetFiles(i.FullName));
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray();
.OfType<Video>() foreach (var extraFolderName in AllExtrasTypesFolderNames)
.Select(item => {
{ var files = folders
// Try to retrieve it from the db. If we don't find it, use the resolved version .Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
var dbItem = LibraryManager.GetItemById(item.Id) as Video; .SelectMany(i => FileSystem.GetFiles(i.FullName));
if (dbItem != null) extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
.OfType<Video>()
.Select(item =>
{ {
item = dbItem; // Try to retrieve it from the db. If we don't find it, use the resolved version
} if (LibraryManager.GetItemById(item.Id) is Video dbItem)
else {
{ item = dbItem;
// item is new }
item.ExtraType = MediaBrowser.Model.Entities.ExtraType.Clip;
}
return item; // Use some hackery to get the extra type based on foldername
Enum.TryParse(extraFolderName.Replace(" ", ""), true, out ExtraType extraType);
item.ExtraType = extraType;
// Sort them so that the list can be easily compared for changes return item;
}).OrderBy(i => i.Path).ToArray();
// Sort them so that the list can be easily compared for changes
}).OrderBy(i => i.Path));
}
return extras.ToArray();
} }
public Task RefreshMetadata(CancellationToken cancellationToken) public Task RefreshMetadata(CancellationToken cancellationToken)
{ {
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken); return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken);
@@ -1481,7 +1500,13 @@ namespace MediaBrowser.Controller.Entities
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{ {
var newExtras = LoadExtras(fileSystemChildren, options.DirectoryService).Concat(LoadThemeVideos(fileSystemChildren, options.DirectoryService)).Concat(LoadThemeSongs(fileSystemChildren, options.DirectoryService)); var extras = LoadExtras(fileSystemChildren, options.DirectoryService);
var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
var themeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
var newExtras = new BaseItem[extras.Length + themeVideos.Length + themeSongs.Length];
extras.CopyTo(newExtras, 0);
themeVideos.CopyTo(newExtras, extras.Length);
themeSongs.CopyTo(newExtras, extras.Length + themeVideos.Length);
var newExtraIds = newExtras.Select(i => i.Id).ToArray(); var newExtraIds = newExtras.Select(i => i.Id).ToArray();
@@ -1493,7 +1518,15 @@ namespace MediaBrowser.Controller.Entities
var tasks = newExtras.Select(i => var tasks = newExtras.Select(i =>
{ {
return RefreshMetadataForOwnedItem(i, true, new MetadataRefreshOptions(options), cancellationToken); var subOptions = new MetadataRefreshOptions(options);
if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
{
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
subOptions.ForceSave = true;
}
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
}); });
await Task.WhenAll(tasks).ConfigureAwait(false); await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -53,7 +53,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly int DefaultImageExtractionTimeoutMs; private readonly int DefaultImageExtractionTimeoutMs;
private readonly string StartupOptionFFmpegPath; private readonly string StartupOptionFFmpegPath;
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2);
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>(); private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
@@ -582,19 +582,27 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
bool ranToCompletion; bool ranToCompletion;
StartProcess(processWrapper); await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
if (timeoutMs <= 0)
{ {
timeoutMs = DefaultImageExtractionTimeoutMs; StartProcess(processWrapper);
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
if (timeoutMs <= 0)
{
timeoutMs = DefaultImageExtractionTimeoutMs;
}
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
if (!ranToCompletion)
{
StopProcess(processWrapper, 1000);
}
} }
finally
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
if (!ranToCompletion)
{ {
StopProcess(processWrapper, 1000); _thumbnailResourcePool.Release();
} }
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
@@ -625,7 +633,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
return time.ToString(@"hh\:mm\:ss\.fff", UsCulture); return time.ToString(@"hh\:mm\:ss\.fff", UsCulture);
} }
public async Task ExtractVideoImagesOnInterval(string[] inputFiles, public async Task ExtractVideoImagesOnInterval(
string[] inputFiles,
string container, string container,
MediaStream videoStream, MediaStream videoStream,
MediaProtocol protocol, MediaProtocol protocol,
@@ -636,8 +645,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
int? maxWidth, int? maxWidth,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var resourcePool = _thumbnailResourcePool;
var inputArgument = GetInputArgument(inputFiles, protocol); var inputArgument = GetInputArgument(inputFiles, protocol);
var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture); var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture);
@@ -701,7 +708,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments); _logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
bool ranToCompletion = false; bool ranToCompletion = false;
@@ -742,7 +749,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
} }
finally finally
{ {
resourcePool.Release(); _thumbnailResourcePool.Release();
} }
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
@@ -25,6 +25,11 @@ namespace MediaBrowser.Model.System
/// </summary> /// </summary>
/// <value>The version.</value> /// <value>The version.</value>
public string Version { get; set; } public string Version { get; set; }
/// <summary>
/// The product name. This is the AssemblyProduct name.
/// </summary>
public string ProductName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the operating system. /// Gets or sets the operating system.
-4
View File
@@ -32,10 +32,6 @@ namespace MediaBrowser.Model.System
/// <value>The display name of the operating system.</value> /// <value>The display name of the operating system.</value>
public string OperatingSystemDisplayName { get; set; } public string OperatingSystemDisplayName { get; set; }
/// <summary>
/// The product name. This is the AssemblyProduct name.
/// </summary>
public string ProductName { get; set; }
/// <summary> /// <summary>
/// Get or sets the package name. /// Get or sets the package name.
@@ -336,7 +336,7 @@ namespace MediaBrowser.Providers.Music
} }
using (var subReader = reader.ReadSubtree()) using (var subReader = reader.ReadSubtree())
{ {
return ParseReleaseList(subReader); return ParseReleaseList(subReader).ToList();
} }
} }
default: default:
@@ -110,7 +110,7 @@ namespace MediaBrowser.Providers.Music
} }
using (var subReader = reader.ReadSubtree()) using (var subReader = reader.ReadSubtree())
{ {
return ParseArtistList(subReader); return ParseArtistList(subReader).ToList();
} }
} }
default: default:
@@ -24,24 +24,28 @@ namespace MediaBrowser.Providers.TV.TheTVDB
{ {
_cache = memoryCache; _cache = memoryCache;
_tvDbClient = new TvDbClient(); _tvDbClient = new TvDbClient();
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey);
_tokenCreatedAt = DateTime.Now;
} }
public TvDbClient TvDbClient private TvDbClient TvDbClient
{ {
get get
{ {
if (string.IsNullOrEmpty(_tvDbClient.Authentication.Token))
{
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
_tokenCreatedAt = DateTime.Now;
}
// Refresh if necessary // Refresh if necessary
if (_tokenCreatedAt > DateTime.Now.Subtract(TimeSpan.FromHours(20))) if (_tokenCreatedAt < DateTime.Now.Subtract(TimeSpan.FromHours(20)))
{ {
try try
{ {
_tvDbClient.Authentication.RefreshTokenAsync(); _tvDbClient.Authentication.RefreshTokenAsync().GetAwaiter().GetResult();
} }
catch catch
{ {
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey); _tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
} }
_tokenCreatedAt = DateTime.Now; _tokenCreatedAt = DateTime.Now;
@@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB
/// <summary> /// <summary>
/// Class RemoteEpisodeProvider /// Class RemoteEpisodeProvider
/// </summary> /// </summary>
class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{ {
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILogger _logger; private readonly ILogger _logger;
+1 -4
View File
@@ -57,10 +57,9 @@ namespace Mono.Nat.Upnp
req.Url = ss; req.Url = ss;
req.EnableKeepAlive = false; req.EnableKeepAlive = false;
req.RequestContentType = "text/xml"; req.RequestContentType = "text/xml";
req.AppendCharsetToMimeType = true;
req.RequestHeaders.Add("SOAPACTION", "\"" + device.ServiceType + "#" + upnpMethod + "\""); req.RequestHeaders.Add("SOAPACTION", "\"" + device.ServiceType + "#" + upnpMethod + "\"");
string bodyString = "<s:Envelope " req.RequestContent = "<s:Envelope "
+ "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " + "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
+ "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
+ "<s:Body>" + "<s:Body>"
@@ -70,8 +69,6 @@ namespace Mono.Nat.Upnp
+ "</u:" + upnpMethod + ">" + "</u:" + upnpMethod + ">"
+ "</s:Body>" + "</s:Body>"
+ "</s:Envelope>\r\n\r\n"; + "</s:Envelope>\r\n\r\n";
req.RequestContentBytes = System.Text.Encoding.UTF8.GetBytes(bodyString);
return req; return req;
} }
+2 -2
View File
@@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("10.3.0")] [assembly: AssemblyVersion("10.3.7")]
[assembly: AssemblyFileVersion("10.3.0")] [assembly: AssemblyFileVersion("10.3.7")]
+3 -1
View File
@@ -1,12 +1,14 @@
--- ---
# We just wrap `build` so this is really it # We just wrap `build` so this is really it
name: "jellyfin" name: "jellyfin"
version: "10.3.0-rc2" version: "10.3.7"
packages: packages:
- debian-package-x64 - debian-package-x64
- debian-package-armhf - debian-package-armhf
- debian-package-arm64
- ubuntu-package-x64 - ubuntu-package-x64
- ubuntu-package-armhf - ubuntu-package-armhf
- ubuntu-package-arm64
- fedora-package-x64 - fedora-package-x64
- centos-package-x64 - centos-package-x64
- linux-x64 - linux-x64
@@ -18,3 +18,4 @@ rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-
# Move the artifacts out # Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/rpm mkdir -p ${ARTIFACT_DIR}/rpm
mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/ mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
-3
View File
@@ -72,9 +72,6 @@ fi
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the RPMs and copy out to ${package_temporary_dir} # Build the RPMs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Correct ownership on the RPMs (as current user, then as root if that fails)
chown -R "${current_user}" "${package_temporary_dir}" \
|| sudo chown -R "${current_user}" "${package_temporary_dir}"
# Move the RPMs to the output directory # Move the RPMs to the output directory
mkdir -p "${output_dir}" mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/rpm/* "${output_dir}" mv "${package_temporary_dir}"/rpm/* "${output_dir}"
@@ -0,0 +1,43 @@
FROM debian:9
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=2.2
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
# Prepare the cross-toolchain
RUN dpkg --add-architecture arm64 \
&& apt-get update \
&& apt-get install -y cross-gcc-dev \
&& TARGET_LIST="arm64" cross-gcc-gensource 6 \
&& cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \
&& apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
# Link to Debian source dir; mkdir needed or it fails, can't force dest
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
VOLUME ${ARTIFACT_DIR}/
COPY . ${SOURCE_DIR}/
ENTRYPOINT ["/docker-build.sh"]
#ENTRYPOINT ["/bin/bash"]
@@ -0,0 +1,34 @@
FROM debian:9
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=2.2
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=arm64
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
# Link to Debian source dir; mkdir needed or it fails, can't force dest
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
VOLUME ${ARTIFACT_DIR}/
COPY . ${SOURCE_DIR}/
ENTRYPOINT ["/docker-build.sh"]
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
source ../common.build.sh
keep_artifacts="${1}"
WORKDIR="$( pwd )"
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
output_dir="${WORKDIR}/pkg-dist"
current_user="$( whoami )"
image_name="jellyfin-debian_arm64-build"
rm -rf "${package_temporary_dir}" &>/dev/null \
|| sudo rm -rf "${package_temporary_dir}" &>/dev/null
rm -rf "${output_dir}" &>/dev/null \
|| sudo rm -rf "${output_dir}" &>/dev/null
if [[ ${keep_artifacts} == 'n' ]]; then
docker_sudo=""
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
&& [[ ! ${EUID:-1000} -eq 0 ]] \
&& [[ ! ${USER} == "root" ]] \
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
docker_sudo=sudo
fi
${docker_sudo} docker image rm ${image_name} --force
fi
@@ -0,0 +1 @@
docker
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Builds the DEB inside the Docker container
set -o errexit
set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
sed -i '/dotnet-sdk-2.2,/d' debian/control
# Build DEB
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
dpkg-buildpackage -us -uc -aarm64
# Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
source ../common.build.sh
ARCH="$( arch )"
WORKDIR="$( pwd )"
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
output_dir="${WORKDIR}/pkg-dist"
current_user="$( whoami )"
image_name="jellyfin-debian_arm64-build"
# Determine if sudo should be used for Docker
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
&& [[ ! ${EUID:-1000} -eq 0 ]] \
&& [[ ! ${USER} == "root" ]] \
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
docker_sudo="sudo"
else
docker_sudo=""
fi
# Determine which Dockerfile to use
case $ARCH in
'x86_64')
DOCKERFILE="Dockerfile.amd64"
;;
'armv7l')
DOCKERFILE="Dockerfile.arm64"
;;
esac
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"
+1
View File
@@ -0,0 +1 @@
../debian-package-x64/pkg-src
@@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarmhf
# Move the artifacts out # Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/ mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+2 -3
View File
@@ -30,13 +30,12 @@ case $ARCH in
;; ;;
esac esac
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image # Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir} # Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Correct ownership on the DEBs (as current user, then as root if that fails)
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
# Move the DEBs to the output directory # Move the DEBs to the output directory
mkdir -p "${output_dir}" mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}"
@@ -17,3 +17,4 @@ dpkg-buildpackage -us -uc
# Move the artifacts out # Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/ mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+2 -3
View File
@@ -19,13 +19,12 @@ else
docker_sudo="" docker_sudo=""
fi fi
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image # Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the DEBs and copy out to ${package_temporary_dir} # Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Correct ownership on the DEBs (as current user, then as root if that fails)
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
# Move the DEBs to the output directory # Move the DEBs to the output directory
mkdir -p "${output_dir}" mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}"
@@ -1,20 +1,36 @@
#!/bin/bash #!/bin/bash
NAME=jellyfin # restart.sh - Jellyfin server restart script
# Part of the Jellyfin project (https://github.com/jellyfin)
#
# This script restarts the Jellyfin daemon on Linux when using
# the Restart button on the admin dashboard. It supports the
# systemctl, service, and traditional /etc/init.d (sysv) restart
# methods, chosen automatically by which one is found first (in
# that order).
#
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
restart_cmds=( get_service_command() {
"systemctl restart ${NAME}" for command in systemctl service; do
"service ${NAME} restart" if which $command &>/dev/null; then
"/etc/init.d/${NAME} restart" echo $command && return
"s6-svc -t /var/run/s6/services/${NAME}" fi
) done
echo "sysv"
}
for restart_cmd in "${restart_cmds[@]}"; do cmd="$( get_service_command )"
cmd=$(echo "$restart_cmd" | awk '{print $1}') echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
cmd_loc=$(command -v ${cmd}) case $cmd in
if [[ -n "$cmd_loc" ]]; then 'systemctl')
restart_cmd=$(echo "$restart_cmd" | sed -e "s%${cmd}%${cmd_loc}%") echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now
echo "sleep 2; sudo $restart_cmd > /dev/null 2>&1" | at now > /dev/null 2>&1 ;;
exit 0 'service')
fi echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now
done ;;
'sysv')
echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now
;;
esac
exit 0
@@ -1,14 +1,50 @@
jellyfin (10.3.0~rc2) unstable; urgency=medium jellyfin (10.3.7-1) unstable; urgency=medium
* New upstream version 10.3.0-rc2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc2 * New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7
-- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 10 Apr 2019 00:51:14 -0400 -- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 24 Jul 2019 10:48:28 -0400
jellyfin (10.3.0~rc1) unstable; urgency=medium jellyfin (10.3.6-1) unstable; urgency=medium
* New upstream version 10.3.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc1 * New upstream version 10.3.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.6
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 30 Mar 2019 15:47:24 -0400 -- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 06 Jul 2019 13:34:19 -0400
jellyfin (10.3.5-1) unstable; urgency=medium
* New upstream version 10.3.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.5
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 09 Jun 2019 21:47:35 -0400
jellyfin (10.3.4-1) unstable; urgency=medium
* New upstream version 10.3.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.4
-- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 06 Jun 2019 22:45:31 -0400
jellyfin (10.3.3-1) unstable; urgency=medium
* New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 17 May 2019 23:12:08 -0400
jellyfin (10.3.2-1) unstable; urgency=medium
* New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 30 Apr 2019 20:18:44 -0400
jellyfin (10.3.1-1) unstable; urgency=medium
* New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 20 Apr 2019 14:24:07 -0400
jellyfin (10.3.0-1) unstable; urgency=medium
* New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 19 Apr 2019 14:24:29 -0400
jellyfin (10.2.2-1) unstable; urgency=medium jellyfin (10.2.2-1) unstable; urgency=medium
@@ -23,6 +23,6 @@ Depends: at,
jellyfin-ffmpeg, jellyfin-ffmpeg,
libfontconfig1, libfontconfig1,
libfreetype6, libfreetype6,
libssl1.0.0 | libssl1.0.2 libssl1.0.0 | libssl1.0.2 | libssl1.1
Description: Jellyfin is a home media server. Description: Jellyfin is a home media server.
It is built on top of other popular open source technologies such as Service Stack, jQuery, jQuery mobile, and Mono. It features a REST-based api with built-in documentation to facilitate client development. We also have client libraries for our api to enable rapid development. It is built on top of other popular open source technologies such as Service Stack, jQuery, jQuery mobile, and Mono. It features a REST-based api with built-in documentation to facilitate client development. We also have client libraries for our api to enable rapid development.
+10 -3
View File
@@ -6,18 +6,25 @@ SHELL := /bin/bash
HOST_ARCH := $(shell arch) HOST_ARCH := $(shell arch)
BUILD_ARCH := ${DEB_HOST_MULTIARCH} BUILD_ARCH := ${DEB_HOST_MULTIARCH}
ifeq ($(HOST_ARCH),x86_64) ifeq ($(HOST_ARCH),x86_64)
# Building AMD64
DOTNETRUNTIME := debian-x64
ifeq ($(BUILD_ARCH),arm-linux-gnueabihf) ifeq ($(BUILD_ARCH),arm-linux-gnueabihf)
# Cross-building ARM on AMD64 # Cross-building ARM on AMD64
DOTNETRUNTIME := debian-arm DOTNETRUNTIME := debian-arm
else endif
# Building AMD64 ifeq ($(BUILD_ARCH),aarch64-linux-gnu)
DOTNETRUNTIME := debian-x64 # Cross-building ARM on AMD64
DOTNETRUNTIME := debian-arm64
endif endif
endif endif
ifeq ($(HOST_ARCH),armv7l) ifeq ($(HOST_ARCH),armv7l)
# Building ARM # Building ARM
DOTNETRUNTIME := debian-arm DOTNETRUNTIME := debian-arm
endif endif
ifeq ($(HOST_ARCH),arm64)
# Building ARM
DOTNETRUNTIME := debian-arm64
endif
export DH_VERBOSE=1 export DH_VERBOSE=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_CLI_TELEMETRY_OPTOUT=1
@@ -18,3 +18,4 @@ rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-
# Move the artifacts out # Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/rpm mkdir -p ${ARTIFACT_DIR}/rpm
mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/ mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+2 -3
View File
@@ -23,13 +23,12 @@ fi
./create_tarball.sh ./create_tarball.sh
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image # Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the RPMs and copy out to ${package_temporary_dir} # Build the RPMs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Correct ownership on the RPMs (as current user, then as root if that fails)
chown -R "${current_user}" "${package_temporary_dir}" \
|| sudo chown -R "${current_user}" "${package_temporary_dir}"
# Move the RPMs to the output directory # Move the RPMs to the output directory
mkdir -p "${output_dir}" mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/rpm/* "${output_dir}" mv "${package_temporary_dir}"/rpm/* "${output_dir}"
@@ -7,7 +7,7 @@
%endif %endif
Name: jellyfin Name: jellyfin
Version: 10.3.0 Version: 10.3.7
Release: 1%{?dist} Release: 1%{?dist}
Summary: The Free Software Media Browser Summary: The Free Software Media Browser
License: GPLv2 License: GPLv2
@@ -140,10 +140,22 @@ fi
%systemd_postun_with_restart jellyfin.service %systemd_postun_with_restart jellyfin.service
%changelog %changelog
* Wed Apr 10 2019 Jellyfin Packaging Team <packaging@jellyfin.org> * Wed Jul 24 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.0-rc2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc2 - New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7
* Sat Mar 30 2019 Jellyfin Packaging Team <packaging@jellyfin.org> * Sat Jul 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0-rc1 - New upstream version 10.3.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.6
* Sun Jun 09 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.5
* Thu Jun 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.4
* Fri May 17 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
* Tue Apr 30 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
* Sat Apr 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
* Fri Apr 19 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
* Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org> * Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- jellyfin: - jellyfin:
- PR968 Release 10.2.z copr autobuild - PR968 Release 10.2.z copr autobuild
@@ -1,6 +1,36 @@
#!/bin/sh #!/bin/bash
NAME=jellyfin # restart.sh - Jellyfin server restart script
restart_cmd="/usr/bin/systemctl restart ${NAME}" # Part of the Jellyfin project (https://github.com/jellyfin)
echo "sleep 2; sudo $restart_cmd > /dev/null 2>&1" | at now > /dev/null 2>&1 #
exit 0 # This script restarts the Jellyfin daemon on Linux when using
# the Restart button on the admin dashboard. It supports the
# systemctl, service, and traditional /etc/init.d (sysv) restart
# methods, chosen automatically by which one is found first (in
# that order).
#
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
get_service_command() {
for command in systemctl service; do
if which $command &>/dev/null; then
echo $command && return
fi
done
echo "sysv"
}
cmd="$( get_service_command )"
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
case $cmd in
'systemctl')
echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now
;;
'service')
echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now
;;
'sysv')
echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now
;;
esac
exit 0
@@ -0,0 +1,53 @@
FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=2.2
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=amd64
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
# Prepare the cross-toolchain
RUN rm /etc/apt/sources.list \
&& export CODENAME="$( lsb_release -c -s )" \
&& echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
&& echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
&& echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
&& echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
&& echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
&& echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
&& echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
&& echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
&& dpkg --add-architecture arm64 \
&& apt-get update \
&& apt-get install -y cross-gcc-dev \
&& TARGET_LIST="arm64" cross-gcc-gensource 6 \
&& cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \
&& ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
&& apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
# Link to Debian source dir; mkdir needed or it fails, can't force dest
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
VOLUME ${ARTIFACT_DIR}/
COPY . ${SOURCE_DIR}/
ENTRYPOINT ["/docker-build.sh"]
@@ -0,0 +1,34 @@
FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64
ARG ARTIFACT_DIR=/dist
ARG SDK_VERSION=2.2
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV ARCH=arm64
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
# Link to docker-build script
RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh
# Link to Debian source dir; mkdir needed or it fails, can't force dest
RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
VOLUME ${ARTIFACT_DIR}/
COPY . ${SOURCE_DIR}/
ENTRYPOINT ["/docker-build.sh"]
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
source ../common.build.sh
keep_artifacts="${1}"
WORKDIR="$( pwd )"
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
output_dir="${WORKDIR}/pkg-dist"
current_user="$( whoami )"
image_name="jellyfin-ubuntu-build"
rm -rf "${package_temporary_dir}" &>/dev/null \
|| sudo rm -rf "${package_temporary_dir}" &>/dev/null
rm -rf "${output_dir}" &>/dev/null \
|| sudo rm -rf "${output_dir}" &>/dev/null
if [[ ${keep_artifacts} == 'n' ]]; then
docker_sudo=""
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
&& [[ ! ${EUID:-1000} -eq 0 ]] \
&& [[ ! ${USER} == "root" ]] \
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
docker_sudo=sudo
fi
${docker_sudo} docker image rm ${image_name} --force
fi
@@ -0,0 +1 @@
docker
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Builds the DEB inside the Docker container
set -o errexit
set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
sed -i '/dotnet-sdk-2.2,/d' debian/control
# Build DEB
export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
dpkg-buildpackage -us -uc -aarm64
# Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
source ../common.build.sh
ARCH="$( arch )"
WORKDIR="$( pwd )"
package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
output_dir="${WORKDIR}/pkg-dist"
current_user="$( whoami )"
image_name="jellyfin-ubuntu_arm64-build"
# Determine if sudo should be used for Docker
if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
&& [[ ! ${EUID:-1000} -eq 0 ]] \
&& [[ ! ${USER} == "root" ]] \
&& [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
docker_sudo="sudo"
else
docker_sudo=""
fi
# Determine which Dockerfile to use
case $ARCH in
'x86_64')
DOCKERFILE="Dockerfile.amd64"
;;
'armv7l')
DOCKERFILE="Dockerfile.arm64"
;;
esac
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Move the DEBs to the output directory
mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}"
+1
View File
@@ -0,0 +1 @@
../debian-package-x64/pkg-src
@@ -18,3 +18,4 @@ dpkg-buildpackage -us -uc -aarmhf
# Move the artifacts out # Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/ mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+2 -3
View File
@@ -30,13 +30,12 @@ case $ARCH in
;; ;;
esac esac
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image # Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE}
# Build the DEBs and copy out to ${package_temporary_dir} # Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Correct ownership on the DEBs (as current user, then as root if that fails)
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
# Move the DEBs to the output directory # Move the DEBs to the output directory
mkdir -p "${output_dir}" mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}"
@@ -17,3 +17,4 @@ dpkg-buildpackage -us -uc
# Move the artifacts out # Move the artifacts out
mkdir -p ${ARTIFACT_DIR}/deb mkdir -p ${ARTIFACT_DIR}/deb
mv /jellyfin_* ${ARTIFACT_DIR}/deb/ mv /jellyfin_* ${ARTIFACT_DIR}/deb/
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+2 -3
View File
@@ -19,13 +19,12 @@ else
docker_sudo="" docker_sudo=""
fi fi
# Prepare temporary package dir
mkdir -p "${package_temporary_dir}"
# Set up the build environment Docker image # Set up the build environment Docker image
${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
# Build the DEBs and copy out to ${package_temporary_dir} # Build the DEBs and copy out to ${package_temporary_dir}
${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
# Correct ownership on the DEBs (as current user, then as root if that fails)
chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
|| sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
# Move the DEBs to the output directory # Move the DEBs to the output directory
mkdir -p "${output_dir}" mkdir -p "${output_dir}"
mv "${package_temporary_dir}"/deb/* "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}"