瀏覽代碼

Merge branch 'master' into warn17

Bond-009 5 年之前
父節點
當前提交
94fe9b8f6d
共有 100 個文件被更改,包括 1396 次插入603 次删除
  1. 119 27
      CONTRIBUTORS.md
  2. 17 3
      Dockerfile
  3. 28 3
      Dockerfile.arm
  4. 18 3
      Dockerfile.arm64
  5. 5 1
      Emby.Dlna/ConnectionManager/ConnectionManager.cs
  6. 1 1
      Emby.Dlna/ContentDirectory/ContentDirectory.cs
  7. 14 5
      Emby.Dlna/Main/DlnaEntryPoint.cs
  8. 4 1
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs
  9. 1 1
      Emby.Dlna/Service/BaseService.cs
  10. 30 22
      Emby.Notifications/Api/NotificationsService.cs
  11. 3 0
      Emby.Notifications/CoreNotificationTypes.cs
  12. 14 0
      Emby.Notifications/Emby.Notifications.csproj
  13. 4 1
      Emby.Notifications/NotificationConfigurationFactory.cs
  14. 80 40
      Emby.Notifications/NotificationEntryPoint.cs
  15. 28 11
      Emby.Notifications/NotificationManager.cs
  16. 1 0
      Emby.Photos/Emby.Photos.csproj
  17. 4 6
      Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
  18. 19 26
      Emby.Server.Implementations/ApplicationHost.cs
  19. 5 1
      Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
  20. 4 1
      Emby.Server.Implementations/Collections/CollectionManager.cs
  21. 0 1
      Emby.Server.Implementations/ConfigurationOptions.cs
  22. 1 1
      Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
  23. 4 1
      Emby.Server.Implementations/Devices/DeviceManager.cs
  24. 6 1
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  25. 21 19
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  26. 2 7
      Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
  27. 3 12
      Emby.Server.Implementations/EntryPoints/StartupWizard.cs
  28. 1 5
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  29. 23 21
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  30. 1 1
      Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
  31. 15 12
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  32. 2 0
      Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs
  33. 21 19
      Emby.Server.Implementations/IO/FileRefresher.cs
  34. 1 2
      Emby.Server.Implementations/Library/LibraryManager.cs
  35. 5 1
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  36. 33 27
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  37. 5 1
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  38. 1 1
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  39. 4 1
      Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
  40. 4 1
      Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
  41. 4 1
      Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
  42. 4 1
      Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
  43. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  44. 1 2
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  45. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  46. 5 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
  47. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
  48. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
  49. 13 5
      Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
  50. 4 0
      Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
  51. 3 0
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  52. 8 1
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  53. 6 3
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  54. 3 0
      Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
  55. 3 0
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  56. 1 0
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  57. 22 26
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  58. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  59. 4 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  60. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  61. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  62. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
  63. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  64. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  65. 3 0
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  66. 13 13
      Emby.Server.Implementations/Localization/Core/ar.json
  67. 1 1
      Emby.Server.Implementations/Localization/Core/da.json
  68. 5 5
      Emby.Server.Implementations/Localization/Core/de.json
  69. 8 1
      Emby.Server.Implementations/Localization/Core/es_DO.json
  70. 20 20
      Emby.Server.Implementations/Localization/Core/fi.json
  71. 30 30
      Emby.Server.Implementations/Localization/Core/he.json
  72. 5 5
      Emby.Server.Implementations/Localization/Core/hu.json
  73. 2 1
      Emby.Server.Implementations/Localization/Core/id.json
  74. 2 2
      Emby.Server.Implementations/Localization/Core/it.json
  75. 96 0
      Emby.Server.Implementations/Localization/Core/lv.json
  76. 96 0
      Emby.Server.Implementations/Localization/Core/mk.json
  77. 5 5
      Emby.Server.Implementations/Localization/Core/ms.json
  78. 2 2
      Emby.Server.Implementations/Localization/Core/nl.json
  79. 40 1
      Emby.Server.Implementations/Localization/Core/nn.json
  80. 25 25
      Emby.Server.Implementations/Localization/Core/pt.json
  81. 16 16
      Emby.Server.Implementations/Localization/Core/sv.json
  82. 2 2
      Emby.Server.Implementations/Localization/Core/zh-CN.json
  83. 2 2
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  84. 4 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  85. 4 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
  86. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
  87. 2 3
      Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
  88. 0 99
      Emby.Server.Implementations/Sorting/AlphanumComparator.cs
  89. 2 5
      Jellyfin.Server/CoreAppHost.cs
  90. 28 0
      Jellyfin.Server/Migrations/IMigrationRoutine.cs
  91. 24 0
      Jellyfin.Server/Migrations/MigrationOptions.cs
  92. 73 0
      Jellyfin.Server/Migrations/MigrationRunner.cs
  93. 20 0
      Jellyfin.Server/Migrations/MigrationsFactory.cs
  94. 24 0
      Jellyfin.Server/Migrations/MigrationsListStore.cs
  95. 73 0
      Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
  96. 35 0
      Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
  97. 61 32
      Jellyfin.Server/Program.cs
  98. 7 1
      Jellyfin.Server/Resources/Configuration/logging.json
  99. 1 1
      MediaBrowser.Api/ApiEntryPoint.cs
  100. 1 1
      MediaBrowser.Api/Library/LibraryService.cs

+ 119 - 27
CONTRIBUTORS.md

@@ -1,40 +1,132 @@
 # Jellyfin Contributors
 
- - [JoshuaBoniface](https://github.com/joshuaboniface)
- - [nvllsvm](https://github.com/nvllsvm)
- - [JustAMan](https://github.com/JustAMan)
+ - [97carmine](https://github.com/97carmine)
+ - [Abbe98](https://github.com/Abbe98)
+ - [agrenott](https://github.com/agrenott)
+ - [AndreCarvalho](https://github.com/AndreCarvalho)
+ - [anthonylavado](https://github.com/anthonylavado)
+ - [Artiume](https://github.com/Artiume)
+ - [AThomsen](https://github.com/AThomsen)
+ - [bilde2910](https://github.com/bilde2910)
+ - [bfayers](https://github.com/bfayers)
+ - [BnMcG](https://github.com/BnMcG)
+ - [Bond-009](https://github.com/Bond-009)
+ - [brianjmurrell](https://github.com/brianjmurrell)
+ - [bugfixin](https://github.com/bugfixin)
+ - [chaosinnovator](https://github.com/chaosinnovator)
+ - [ckcr4lyf](https://github.com/ckcr4lyf)
+ - [crankdoofus](https://github.com/crankdoofus)
+ - [crobibero](https://github.com/crobibero)
+ - [cromefire](https://github.com/cromefire)
+ - [cryptobank](https://github.com/cryptobank)
+ - [cvium](https://github.com/cvium)
+ - [dannymichel](https://github.com/dannymichel)
+ - [DaveChild](https://github.com/DaveChild)
  - [dcrdev](https://github.com/dcrdev)
+ - [dhartung](https://github.com/dhartung)
+ - [dinki](https://github.com/dinki)
+ - [dkanada](https://github.com/dkanada)
+ - [dlahoti](https://github.com/dlahoti)
+ - [dmitrylyzo](https://github.com/dmitrylyzo)
+ - [DMouse10462](https://github.com/DMouse10462)
+ - [DrPandemic](https://github.com/DrPandemic)
  - [EraYaN](https://github.com/EraYaN)
+ - [escabe](https://github.com/escabe)
+ - [excelite](https://github.com/excelite)
+ - [fasheng](https://github.com/fasheng)
+ - [ferferga](https://github.com/ferferga)
+ - [fhriley](https://github.com/fhriley)
  - [flemse](https://github.com/flemse)
- - [bfayers](https://github.com/bfayers)
- - [Bond_009](https://github.com/Bond-009)
- - [AnthonyLavado](https://github.com/anthonylavado)
- - [sparky8251](https://github.com/sparky8251)
- - [LeoVerto](https://github.com/LeoVerto)
+ - [Froghut](https://github.com/Froghut)
+ - [fruhnow](https://github.com/fruhnow)
+ - [geilername](https://github.com/geilername)
+ - [gnattu](https://github.com/gnattu)
  - [grafixeyehero](https://github.com/grafixeyehero)
- - [cvium](https://github.com/cvium)
- - [wtayl0r](https://github.com/wtayl0r)
- - [TtheCreator](https://github.com/Tthecreator)
- - [dkanada](https://github.com/dkanada)
- - [LogicalPhallacy](https://github.com/LogicalPhallacy/)
- - [RazeLighter777](https://github.com/RazeLighter777)
- - [WillWill56](https://github.com/WillWill56)
+ - [h1nk](https://github.com/h1nk)
+ - [hawken93](https://github.com/hawken93)
+ - [HelloWorld017](https://github.com/HelloWorld017)
+ - [jftuga](https://github.com/jftuga)
+ - [joern-h](https://github.com/joern-h)
+ - [joshuaboniface](https://github.com/joshuaboniface)
+ - [JustAMan](https://github.com/JustAMan)
+ - [justinfenn](https://github.com/justinfenn)
+ - [KerryRJ](https://github.com/KerryRJ)
+ - [Larvitar](https://github.com/Larvitar)
+ - [LeoVerto](https://github.com/LeoVerto)
  - [Liggy](https://github.com/Liggy)
- - [fruhnow](https://github.com/fruhnow)
+ - [LogicalPhallacy](https://github.com/LogicalPhallacy)
+ - [loli10K](https://github.com/loli10K)
+ - [lostmypillow](https://github.com/lostmypillow)
  - [Lynxy](https://github.com/Lynxy)
- - [fasheng](https://github.com/fasheng)
- - [ploughpuff](https://github.com/ploughpuff)
- - [pjeanjean](https://github.com/pjeanjean)
- - [DrPandemic](https://github.com/drpandemic)
- - [joern-h](https://github.com/joern-h)
- - [Khinenw](https://github.com/HelloWorld017)
- - [fhriley](https://github.com/fhriley)
- - [nevado](https://github.com/nevado)
+ - [ManfredRichthofen](https://github.com/ManfredRichthofen)
+ - [Marenz](https://github.com/Marenz)
+ - [marius-luca-87](https://github.com/marius-luca-87)
  - [mark-monteiro](https://github.com/mark-monteiro)
- - [ullmie02](https://github.com/ullmie02)
- - [geilername](https://github.com/geilername)
+ - [Matt07211](https://github.com/Matt07211)
+ - [mcarlton00](https://github.com/mcarlton00)
+ - [mitchfizz05](https://github.com/mitchfizz05)
+ - [MrTimscampi](https://github.com/MrTimscampi)
+ - [n8225](https://github.com/n8225)
+ - [Narfinger](https://github.com/Narfinger)
+ - [NathanPickard](https://github.com/NathanPickard)
+ - [neilsb](https://github.com/neilsb)
+ - [nevado](https://github.com/nevado)
+ - [Nickbert7](https://github.com/Nickbert7)
+ - [nvllsvm](https://github.com/nvllsvm)
+ - [nyanmisaka](https://github.com/nyanmisaka)
+ - [oddstr13](https://github.com/oddstr13)
+ - [petermcneil](https://github.com/petermcneil)
+ - [Phlogi](https://github.com/Phlogi)
+ - [pjeanjean](https://github.com/pjeanjean)
+ - [ploughpuff](https://github.com/ploughpuff)
  - [pR0Ps](https://github.com/pR0Ps)
-
+ - [PrplHaz4](https://github.com/PrplHaz4)
+ - [RazeLighter777](https://github.com/RazeLighter777)
+ - [redSpoutnik](https://github.com/redSpoutnik)
+ - [ringmatter](https://github.com/ringmatter)
+ - [ryan-hartzell](https://github.com/ryan-hartzell)
+ - [s0urcelab](https://github.com/s0urcelab)
+ - [sachk](https://github.com/sachk)
+ - [sammyrc34](https://github.com/sammyrc34)
+ - [samuel9554](https://github.com/samuel9554)
+ - [scheidleon](https://github.com/scheidleon)
+ - [sebPomme](https://github.com/sebPomme)
+ - [SenorSmartyPants](https://github.com/SenorSmartyPants)
+ - [shemanaev](https://github.com/shemanaev)
+ - [skaro13](https://github.com/skaro13)
+ - [sl1288](https://github.com/sl1288)
+ - [sorinyo2004](https://github.com/sorinyo2004)
+ - [sparky8251](https://github.com/sparky8251)
+ - [stanionascu](https://github.com/stanionascu)
+ - [stevehayles](https://github.com/stevehayles)
+ - [SuperSandro2000](https://github.com/SuperSandro2000)
+ - [tbraeutigam](https://github.com/tbraeutigam)
+ - [teacupx](https://github.com/teacupx)
+ - [Terror-Gene](https://github.com/Terror-Gene)
+ - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
+ - [ThibaultNocchi](https://github.com/ThibaultNocchi)
+ - [thornbill](https://github.com/thornbill)
+ - [ThreeFive-O](https://github.com/ThreeFive-O)
+ - [TrisMcC](https://github.com/TrisMcC)
+ - [trumblejoe](https://github.com/trumblejoe)
+ - [TtheCreator](https://github.com/TtheCreator)
+ - [twinkybot](https://github.com/twinkybot)
+ - [Ullmie02](https://github.com/Ullmie02)
+ - [Unhelpful](https://github.com/Unhelpful)
+ - [viaregio](https://github.com/viaregio)
+ - [vitorsemeano](https://github.com/vitorsemeano)
+ - [voodoos](https://github.com/voodoos)
+ - [whooo](https://github.com/whooo)
+ - [WiiPlayer2](https://github.com/WiiPlayer2)
+ - [WillWill56](https://github.com/WillWill56)
+ - [wtayl0r](https://github.com/wtayl0r)
+ - [Wuerfelbecher](https://github.com/Wuerfelbecher)
+ - [Wunax](https://github.com/Wunax)
+ - [WWWesten](https://github.com/WWWesten)
+ - [WX9yMOXWId](https://github.com/WX9yMOXWId)
+ - [xosdy](https://github.com/xosdy)
+ - [XVicarious](https://github.com/XVicarious)
+ - [YouKnowBlom](https://github.com/YouKnowBlom)
 
 # Emby Contributors
 

+ 17 - 3
Dockerfile

@@ -21,6 +21,13 @@ RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --
 FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
 FROM debian:buster-slim
 
+# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
+ARG DEBIAN_FRONTEND="noninteractive"
+# http://stackoverflow.com/questions/48162574/ddg#49462622
+ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
+# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
+ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
+
 COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
@@ -31,9 +38,16 @@ COPY --from=web-builder /dist /jellyfin/jellyfin-web
 #   mesa-va-drivers: needed for VAAPI
 RUN apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y \
-   libfontconfig1 libgomp1 libva-drm2 mesa-va-drivers openssl ca-certificates \
- && apt-get clean autoclean \
- && apt-get autoremove \
+   libfontconfig1 \
+   libgomp1 \
+   libva-drm2 \
+   mesa-va-drivers \
+   openssl \
+   ca-certificates \
+   vainfo \
+   i965-va-driver \
+ && apt-get clean autoclean -y\
+ && apt-get autoremove -y\
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media \

+ 28 - 3
Dockerfile.arm

@@ -27,10 +27,35 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
 FROM arm32v7/debian:buster-slim
+
+# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
+ARG DEBIAN_FRONTEND="noninteractive"
+# http://stackoverflow.com/questions/48162574/ddg#49462622
+ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
+# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
+ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
+
 COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
 RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
- libssl-dev ca-certificates \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
+ curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
+ curl -s https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
+ echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
+ echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
+ apt-get update && \
+ apt-get install --no-install-recommends --no-install-suggests -y \
+ jellyfin-ffmpeg \
+ libssl-dev \
+ libfontconfig1 \
+ libfreetype6 \
+ libomxil-bellagio0 \
+ libomxil-bellagio-bin \
+ libraspberrypi0 \
+ vainfo \
+ libva2 \
+ && apt-get remove curl gnupg -y \
+ && apt-get clean autoclean -y \
+ && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media
@@ -44,4 +69,4 @@ VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \
     "--datadir", "/config", \
     "--cachedir", "/cache", \
-    "--ffmpeg", "/usr/bin/ffmpeg"]
+    "--ffmpeg", "/usr/lib/jellyfin-ffmpeg"]

+ 18 - 3
Dockerfile.arm64

@@ -26,10 +26,25 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
 FROM arm64v8/debian:buster-slim
+
+# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
+ARG DEBIAN_FRONTEND="noninteractive"
+# http://stackoverflow.com/questions/48162574/ddg#49462622
+ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
+# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
+ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
+
 COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
-RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
- libssl-dev ca-certificates \
+RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \ 
+ ffmpeg \
+ libssl-dev \
+ ca-certificates \
+ libfontconfig1 \
+ libfreetype6 \
+ libomxil-bellagio0 \
+ libomxil-bellagio-bin \
+ && apt-get clean autoclean -y \
+ && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
  && chmod 777 /cache /config /media

+ 5 - 1
Emby.Dlna/ConnectionManager/ConnectionManager.cs

@@ -15,7 +15,11 @@ namespace Emby.Dlna.ConnectionManager
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
 
-        public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient)
+        public ConnectionManager(
+            IDlnaManager dlna,
+            IServerConfigurationManager config,
+            ILogger<ConnectionManager> logger,
+            IHttpClient httpClient)
             : base(logger, httpClient)
         {
             _dlna = dlna;

+ 1 - 1
Emby.Dlna/ContentDirectory/ContentDirectory.cs

@@ -37,7 +37,7 @@ namespace Emby.Dlna.ContentDirectory
             ILibraryManager libraryManager,
             IServerConfigurationManager config,
             IUserManager userManager,
-            ILogger logger,
+            ILogger<ContentDirectory> logger,
             IHttpClient httpClient,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,

+ 14 - 5
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -1,8 +1,8 @@
 #pragma warning disable CS1591
 
 using System;
-using System.Net.Sockets;
 using System.Globalization;
+using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.PlayTo;
@@ -26,7 +26,7 @@ using MediaBrowser.Model.System;
 using Microsoft.Extensions.Logging;
 using Rssdp;
 using Rssdp.Infrastructure;
-using OperatingSystem =  MediaBrowser.Common.System.OperatingSystem;
+using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 
 namespace Emby.Dlna.Main
 {
@@ -58,7 +58,9 @@ namespace Emby.Dlna.Main
         private ISsdpCommunicationsServer _communicationsServer;
 
         internal IContentDirectory ContentDirectory { get; private set; }
+
         internal IConnectionManager ConnectionManager { get; private set; }
+
         internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
 
         public static DlnaEntryPoint Current;
@@ -106,7 +108,7 @@ namespace Emby.Dlna.Main
                 libraryManager,
                 config,
                 userManager,
-                _logger,
+                loggerFactory.CreateLogger<ContentDirectory.ContentDirectory>(),
                 httpClient,
                 localizationManager,
                 mediaSourceManager,
@@ -114,9 +116,16 @@ namespace Emby.Dlna.Main
                 mediaEncoder,
                 tvSeriesManager);
 
-            ConnectionManager = new ConnectionManager.ConnectionManager(dlnaManager, config, _logger, httpClient);
+            ConnectionManager = new ConnectionManager.ConnectionManager(
+                dlnaManager,
+                config,
+                loggerFactory.CreateLogger<ConnectionManager.ConnectionManager>(),
+                httpClient);
 
-            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(_logger, httpClient, config);
+            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(
+                loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrar>(),
+                httpClient,
+                config);
             Current = this;
         }
 

+ 4 - 1
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs

@@ -12,7 +12,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
     {
         private readonly IServerConfigurationManager _config;
 
-        public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config)
+        public MediaReceiverRegistrar(
+            ILogger<MediaReceiverRegistrar> logger,
+            IHttpClient httpClient,
+            IServerConfigurationManager config)
             : base(logger, httpClient)
         {
             _config = config;

+ 1 - 1
Emby.Dlna/Service/BaseService.cs

@@ -12,7 +12,7 @@ namespace Emby.Dlna.Service
         protected IHttpClient HttpClient;
         protected ILogger Logger;
 
-        protected BaseService(ILogger logger, IHttpClient httpClient)
+        protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
         {
             Logger = logger;
             HttpClient = httpClient;

+ 30 - 22
Emby.Notifications/Api/NotificationsService.cs

@@ -1,5 +1,11 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1402
+#pragma warning disable SA1600
+#pragma warning disable SA1649
+
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -16,7 +22,7 @@ namespace Emby.Notifications.Api
     public class GetNotifications : IReturn<NotificationResult>
     {
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
 
         [ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
         public bool? IsRead { get; set; }
@@ -30,32 +36,34 @@ namespace Emby.Notifications.Api
 
     public class Notification
     {
-        public string Id { get; set; }
+        public string Id { get; set; } = string.Empty;
 
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
 
         public DateTime Date { get; set; }
 
         public bool IsRead { get; set; }
 
-        public string Name { get; set; }
+        public string Name { get; set; } = string.Empty;
 
-        public string Description { get; set; }
+        public string Description { get; set; } = string.Empty;
 
-        public string Url { get; set; }
+        public string Url { get; set; } = string.Empty;
 
         public NotificationLevel Level { get; set; }
     }
 
     public class NotificationResult
     {
-        public Notification[] Notifications { get; set; }
+        public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
+
         public int TotalRecordCount { get; set; }
     }
 
     public class NotificationsSummary
     {
         public int UnreadCount { get; set; }
+
         public NotificationLevel MaxUnreadNotificationLevel { get; set; }
     }
 
@@ -63,7 +71,7 @@ namespace Emby.Notifications.Api
     public class GetNotificationsSummary : IReturn<NotificationsSummary>
     {
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
     }
 
     [Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
@@ -80,16 +88,16 @@ namespace Emby.Notifications.Api
     public class AddAdminNotification : IReturnVoid
     {
         [ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Name { get; set; }
+        public string Name { get; set; } = string.Empty;
 
         [ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Description { get; set; }
+        public string Description { get; set; } = string.Empty;
 
         [ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string ImageUrl { get; set; }
+        public string? ImageUrl { get; set; }
 
         [ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string Url { get; set; }
+        public string? Url { get; set; }
 
         [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
         public NotificationLevel Level { get; set; }
@@ -99,20 +107,20 @@ namespace Emby.Notifications.Api
     public class MarkRead : IReturnVoid
     {
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
 
         [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; }
+        public string Ids { get; set; } = string.Empty;
     }
 
     [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
     public class MarkUnread : IReturnVoid
     {
         [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string UserId { get; set; }
+        public string UserId { get; set; } = string.Empty;
 
         [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
-        public string Ids { get; set; }
+        public string Ids { get; set; } = string.Empty;
     }
 
     [Authenticated]
@@ -127,32 +135,29 @@ namespace Emby.Notifications.Api
             _userManager = userManager;
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotificationTypes request)
         {
             return _notificationManager.GetNotificationTypes();
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotificationServices request)
         {
             return _notificationManager.GetNotificationServices().ToList();
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotificationsSummary request)
         {
             return new NotificationsSummary
             {
-
             };
         }
 
         public Task Post(AddAdminNotification request)
         {
             // This endpoint really just exists as post of a real with sickbeard
-            return AddNotification(request);
-        }
-
-        private Task AddNotification(AddAdminNotification request)
-        {
             var notification = new NotificationRequest
             {
                 Date = DateTime.UtcNow,
@@ -166,14 +171,17 @@ namespace Emby.Notifications.Api
             return _notificationManager.SendNotification(notification, CancellationToken.None);
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public void Post(MarkRead request)
         {
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public void Post(MarkUnread request)
         {
         }
 
+        [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
         public object Get(GetNotifications request)
         {
             return new NotificationResult();

+ 3 - 0
Emby.Notifications/CoreNotificationTypes.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.Linq;

+ 14 - 0
Emby.Notifications/Emby.Notifications.csproj

@@ -4,6 +4,8 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
 
   <ItemGroup>
@@ -16,4 +18,16 @@
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
   </ItemGroup>
 
+  <!-- Code analyzers-->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
 </Project>

+ 4 - 1
Emby.Notifications/NotificationConfigurationFactory.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System.Collections.Generic;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Notifications;
@@ -13,7 +16,7 @@ namespace Emby.Notifications
                 new ConfigurationStore
                 {
                     Key = "notifications",
-                    ConfigurationType = typeof (NotificationOptions)
+                    ConfigurationType = typeof(NotificationOptions)
                 }
             };
         }

+ 80 - 40
Emby.Notifications/Notifications.cs → Emby.Notifications/NotificationEntryPoint.cs

@@ -21,70 +21,85 @@ using Microsoft.Extensions.Logging;
 namespace Emby.Notifications
 {
     /// <summary>
-    /// Creates notifications for various system events
+    /// Creates notifications for various system events.
     /// </summary>
-    public class Notifications : IServerEntryPoint
+    public class NotificationEntryPoint : IServerEntryPoint
     {
         private readonly ILogger _logger;
-
+        private readonly IActivityManager _activityManager;
+        private readonly ILocalizationManager _localization;
         private readonly INotificationManager _notificationManager;
-
         private readonly ILibraryManager _libraryManager;
         private readonly IServerApplicationHost _appHost;
+        private readonly IConfigurationManager _config;
 
-        private Timer LibraryUpdateTimer { get; set; }
         private readonly object _libraryChangedSyncLock = new object();
+        private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
 
-        private readonly IConfigurationManager _config;
-        private readonly ILocalizationManager _localization;
-        private readonly IActivityManager _activityManager;
+        private Timer? _libraryUpdateTimer;
 
         private string[] _coreNotificationTypes;
 
-        public Notifications(
+        private bool _disposed = false;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="NotificationEntryPoint" /> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="activityManager">The activity manager.</param>
+        /// <param name="localization">The localization manager.</param>
+        /// <param name="notificationManager">The notification manager.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="appHost">The application host.</param>
+        /// <param name="config">The configuration manager.</param>
+        public NotificationEntryPoint(
+            ILogger<NotificationEntryPoint> logger,
             IActivityManager activityManager,
             ILocalizationManager localization,
-            ILogger logger,
             INotificationManager notificationManager,
             ILibraryManager libraryManager,
             IServerApplicationHost appHost,
             IConfigurationManager config)
         {
             _logger = logger;
+            _activityManager = activityManager;
+            _localization = localization;
             _notificationManager = notificationManager;
             _libraryManager = libraryManager;
             _appHost = appHost;
             _config = config;
-            _localization = localization;
-            _activityManager = activityManager;
 
             _coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
         }
 
+        /// <inheritdoc />
         public Task RunAsync()
         {
-            _libraryManager.ItemAdded += _libraryManager_ItemAdded;
-            _appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged;
-            _appHost.HasUpdateAvailableChanged += _appHost_HasUpdateAvailableChanged;
-            _activityManager.EntryCreated += _activityManager_EntryCreated;
+            _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
+            _appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
+            _appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
+            _activityManager.EntryCreated += OnActivityManagerEntryCreated;
 
             return Task.CompletedTask;
         }
 
-        private async void _appHost_HasPendingRestartChanged(object sender, EventArgs e)
+        private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
         {
             var type = NotificationType.ServerRestartRequired.ToString();
 
             var notification = new NotificationRequest
             {
                 NotificationType = type,
-                Name = string.Format(_localization.GetLocalizedString("ServerNameNeedsToBeRestarted"), _appHost.Name)
+                Name = string.Format(
+                    CultureInfo.InvariantCulture,
+                    _localization.GetLocalizedString("ServerNameNeedsToBeRestarted"),
+                    _appHost.Name)
             };
 
             await SendNotification(notification, null).ConfigureAwait(false);
         }
 
-        private async void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
         {
             var entry = e.Argument;
 
@@ -117,7 +132,7 @@ namespace Emby.Notifications
             return _config.GetConfiguration<NotificationOptions>("notifications");
         }
 
-        private async void _appHost_HasUpdateAvailableChanged(object sender, EventArgs e)
+        private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e)
         {
             if (!_appHost.HasUpdateAvailable)
             {
@@ -136,8 +151,7 @@ namespace Emby.Notifications
             await SendNotification(notification, null).ConfigureAwait(false);
         }
 
-        private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
-        private void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
+        private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
         {
             if (!FilterItem(e.Item))
             {
@@ -146,14 +160,17 @@ namespace Emby.Notifications
 
             lock (_libraryChangedSyncLock)
             {
-                if (LibraryUpdateTimer == null)
+                if (_libraryUpdateTimer == null)
                 {
-                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, 5000,
-                                                   Timeout.Infinite);
+                    _libraryUpdateTimer = new Timer(
+                        LibraryUpdateTimerCallback,
+                        null,
+                        5000,
+                        Timeout.Infinite);
                 }
                 else
                 {
-                    LibraryUpdateTimer.Change(5000, Timeout.Infinite);
+                    _libraryUpdateTimer.Change(5000, Timeout.Infinite);
                 }
 
                 _itemsAdded.Add(e.Item);
@@ -188,7 +205,8 @@ namespace Emby.Notifications
             {
                 items = _itemsAdded.ToList();
                 _itemsAdded.Clear();
-                DisposeLibraryUpdateTimer();
+                _libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback
+                _libraryUpdateTimer = null;
             }
 
             items = items.Take(10).ToList();
@@ -198,7 +216,10 @@ namespace Emby.Notifications
                 var notification = new NotificationRequest
                 {
                     NotificationType = NotificationType.NewLibraryContent.ToString(),
-                    Name = string.Format(_localization.GetLocalizedString("ValueHasBeenAddedToLibrary"), GetItemName(item)),
+                    Name = string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localization.GetLocalizedString("ValueHasBeenAddedToLibrary"),
+                        GetItemName(item)),
                     Description = item.Overview
                 };
 
@@ -206,6 +227,11 @@ namespace Emby.Notifications
             }
         }
 
+        /// <summary>
+        /// Creates a human readable name for the item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>A human readable name for the item.</returns>
         public static string GetItemName(BaseItem item)
         {
             var name = item.Name;
@@ -219,6 +245,7 @@ namespace Emby.Notifications
                         episode.IndexNumber.Value,
                         name);
                 }
+
                 if (episode.ParentIndexNumber.HasValue)
                 {
                     name = string.Format(
@@ -229,7 +256,6 @@ namespace Emby.Notifications
                 }
             }
 
-
             if (item is IHasSeries hasSeries)
             {
                 name = hasSeries.SeriesName + " - " + name;
@@ -257,7 +283,7 @@ namespace Emby.Notifications
             return name;
         }
 
-        private async Task SendNotification(NotificationRequest notification, BaseItem relatedItem)
+        private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem)
         {
             try
             {
@@ -269,23 +295,37 @@ namespace Emby.Notifications
             }
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
-            DisposeLibraryUpdateTimer();
-
-            _libraryManager.ItemAdded -= _libraryManager_ItemAdded;
-            _appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged;
-            _appHost.HasUpdateAvailableChanged -= _appHost_HasUpdateAvailableChanged;
-            _activityManager.EntryCreated -= _activityManager_EntryCreated;
+            Dispose(true);
+            GC.SuppressFinalize(this);
         }
 
-        private void DisposeLibraryUpdateTimer()
+        /// <summary>
+        /// Releases unmanaged and optionally managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool disposing)
         {
-            if (LibraryUpdateTimer != null)
+            if (_disposed)
+            {
+                return;
+            }
+
+            if (disposing)
             {
-                LibraryUpdateTimer.Dispose();
-                LibraryUpdateTimer = null;
+                _libraryUpdateTimer?.Dispose();
             }
+
+            _libraryUpdateTimer = null;
+
+            _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
+            _appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
+            _appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
+            _activityManager.EntryCreated -= OnActivityManagerEntryCreated;
+
+            _disposed = true;
         }
     }
 }

+ 28 - 11
Emby.Notifications/NotificationManager.cs

@@ -16,20 +16,32 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Notifications
 {
+    /// <summary>
+    /// NotificationManager class.
+    /// </summary>
     public class NotificationManager : INotificationManager
     {
         private readonly ILogger _logger;
         private readonly IUserManager _userManager;
         private readonly IServerConfigurationManager _config;
 
-        private INotificationService[] _services;
-        private INotificationTypeFactory[] _typeFactories;
-
-        public NotificationManager(ILoggerFactory loggerFactory, IUserManager userManager, IServerConfigurationManager config)
+        private INotificationService[] _services = Array.Empty<INotificationService>();
+        private INotificationTypeFactory[] _typeFactories = Array.Empty<INotificationTypeFactory>();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="NotificationManager" /> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="config">The server configuration manager.</param>
+        public NotificationManager(
+            ILogger<NotificationManager> logger,
+            IUserManager userManager,
+            IServerConfigurationManager config)
         {
+            _logger = logger;
             _userManager = userManager;
             _config = config;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
         }
 
         private NotificationOptions GetConfiguration()
@@ -37,12 +49,14 @@ namespace Emby.Notifications
             return _config.GetConfiguration<NotificationOptions>("notifications");
         }
 
+        /// <inheritdoc />
         public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken)
         {
             return SendNotification(request, null, cancellationToken);
         }
 
-        public Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken)
         {
             var notificationType = request.NotificationType;
 
@@ -64,7 +78,8 @@ namespace Emby.Notifications
             return Task.WhenAll(tasks);
         }
 
-        private Task SendNotification(NotificationRequest request,
+        private Task SendNotification(
+            NotificationRequest request,
             INotificationService service,
             IEnumerable<User> users,
             string title,
@@ -79,7 +94,7 @@ namespace Emby.Notifications
             return Task.WhenAll(tasks);
         }
 
-        private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption options)
+        private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption? options)
         {
             if (request.SendToUserMode.HasValue)
             {
@@ -109,7 +124,8 @@ namespace Emby.Notifications
             return request.UserIds;
         }
 
-        private async Task SendNotification(NotificationRequest request,
+        private async Task SendNotification(
+            NotificationRequest request,
             INotificationService service,
             string title,
             string description,
@@ -161,12 +177,14 @@ namespace Emby.Notifications
             return GetConfiguration().IsServiceEnabled(service.Name, notificationType);
         }
 
+        /// <inheritdoc />
         public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories)
         {
             _services = services.ToArray();
             _typeFactories = notificationTypeFactories.ToArray();
         }
 
+        /// <inheritdoc />
         public List<NotificationTypeInfo> GetNotificationTypes()
         {
             var list = _typeFactories.Select(i =>
@@ -180,7 +198,6 @@ namespace Emby.Notifications
                     _logger.LogError(ex, "Error in GetNotificationTypes");
                     return new List<NotificationTypeInfo>();
                 }
-
             }).SelectMany(i => i).ToList();
 
             var config = GetConfiguration();
@@ -193,13 +210,13 @@ namespace Emby.Notifications
             return list;
         }
 
+        /// <inheritdoc />
         public IEnumerable<NameIdPair> GetNotificationServices()
         {
             return _services.Select(i => new NameIdPair
             {
                 Name = i.Name,
                 Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture)
-
             }).OrderBy(i => i.Name);
         }
     }

+ 1 - 0
Emby.Photos/Emby.Photos.csproj

@@ -17,6 +17,7 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
 
   <!-- Code Analyzers-->

+ 4 - 6
Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs

@@ -28,7 +28,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Activity
 {
-    public class ActivityLogEntryPoint : IServerEntryPoint
+    public sealed class ActivityLogEntryPoint : IServerEntryPoint
     {
         private readonly ILogger _logger;
         private readonly IInstallationManager _installationManager;
@@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.Activity
         private readonly ILocalizationManager _localization;
         private readonly ISubtitleManager _subManager;
         private readonly IUserManager _userManager;
-        private readonly IServerApplicationHost _appHost;
         private readonly IDeviceManager _deviceManager;
 
         /// <summary>
@@ -63,8 +62,7 @@ namespace Emby.Server.Implementations.Activity
             ILocalizationManager localization,
             IInstallationManager installationManager,
             ISubtitleManager subManager,
-            IUserManager userManager,
-            IServerApplicationHost appHost)
+            IUserManager userManager)
         {
             _logger = logger;
             _sessionManager = sessionManager;
@@ -75,7 +73,6 @@ namespace Emby.Server.Implementations.Activity
             _installationManager = installationManager;
             _subManager = subManager;
             _userManager = userManager;
-            _appHost = appHost;
         }
 
         public Task RunAsync()
@@ -140,7 +137,7 @@ namespace Emby.Server.Implementations.Activity
                     CultureInfo.InvariantCulture,
                     _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
                     e.Provider,
-                    Notifications.Notifications.GetItemName(e.Item)),
+                    Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)),
                 Type = "SubtitleDownloadFailure",
                 ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
                 ShortOverview = e.Exception.Message
@@ -532,6 +529,7 @@ namespace Emby.Server.Implementations.Activity
         private void CreateLogEntry(ActivityLogEntry entry)
             => _activityManager.Create(entry);
 
+        /// <inheritdoc />
         public void Dispose()
         {
             _taskManager.TaskCompleted -= OnTaskCompleted;

+ 19 - 26
Emby.Server.Implementations/ApplicationHost.cs

@@ -118,7 +118,6 @@ namespace Emby.Server.Implementations
     public abstract class ApplicationHost : IServerApplicationHost, IDisposable
     {
         private SqliteUserRepository _userRepository;
-
         private SqliteDisplayPreferencesRepository _displayPreferencesRepository;
 
         /// <summary>
@@ -166,10 +165,9 @@ namespace Emby.Server.Implementations
         public bool IsShuttingDown { get; private set; }
 
         /// <summary>
-        /// Gets or sets the logger.
+        /// Gets the logger.
         /// </summary>
-        /// <value>The logger.</value>
-        protected ILogger Logger { get; set; }
+        protected ILogger Logger { get; }
 
         private IPlugin[] _plugins;
 
@@ -180,10 +178,9 @@ namespace Emby.Server.Implementations
         public IReadOnlyList<IPlugin> Plugins => _plugins;
 
         /// <summary>
-        /// Gets or sets the logger factory.
+        /// Gets the logger factory.
         /// </summary>
-        /// <value>The logger factory.</value>
-        public ILoggerFactory LoggerFactory { get; protected set; }
+        protected ILoggerFactory LoggerFactory { get; }
 
         /// <summary>
         /// Gets or sets the application paths.
@@ -327,8 +324,6 @@ namespace Emby.Server.Implementations
 
         private IMediaSourceManager MediaSourceManager { get; set; }
 
-        private readonly IConfiguration _configuration;
-
         /// <summary>
         /// Gets the installation manager.
         /// </summary>
@@ -366,11 +361,8 @@ namespace Emby.Server.Implementations
             IStartupOptions options,
             IFileSystem fileSystem,
             IImageEncoder imageEncoder,
-            INetworkManager networkManager,
-            IConfiguration configuration)
+            INetworkManager networkManager)
         {
-            _configuration = configuration;
-
             XmlSerializer = new MyXmlSerializer();
 
             NetworkManager = networkManager;
@@ -586,7 +578,8 @@ namespace Emby.Server.Implementations
             }
         }
 
-        public async Task InitAsync(IServiceCollection serviceCollection)
+        /// <inheritdoc/>
+        public async Task InitAsync(IServiceCollection serviceCollection, IConfiguration startupConfig)
         {
             HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
             HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@@ -619,7 +612,7 @@ namespace Emby.Server.Implementations
 
             DiscoverTypes();
 
-            await RegisterResources(serviceCollection).ConfigureAwait(false);
+            await RegisterResources(serviceCollection, startupConfig).ConfigureAwait(false);
 
             ContentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath;
             if (string.IsNullOrEmpty(ContentRoot))
@@ -651,14 +644,14 @@ namespace Emby.Server.Implementations
             var response = context.Response;
             var localPath = context.Request.Path.ToString();
 
-            var req = new WebSocketSharpRequest(request, response, request.Path, Logger);
+            var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>());
             await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
         }
 
         /// <summary>
         /// Registers resources that classes will depend on
         /// </summary>
-        protected async Task RegisterResources(IServiceCollection serviceCollection)
+        protected async Task RegisterResources(IServiceCollection serviceCollection, IConfiguration startupConfig)
         {
             serviceCollection.AddMemoryCache();
 
@@ -667,13 +660,10 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 
-            serviceCollection.AddSingleton<IConfiguration>(_configuration);
-
             serviceCollection.AddSingleton(JsonSerializer);
 
-            serviceCollection.AddSingleton(LoggerFactory);
-            serviceCollection.AddLogging();
-            serviceCollection.AddSingleton(Logger);
+            // TODO: Support for injecting ILogger should be deprecated in favour of ILogger<T> and this removed
+            serviceCollection.AddSingleton<ILogger>(Logger);
 
             serviceCollection.AddSingleton(FileSystemManager);
             serviceCollection.AddSingleton<TvDbClientManager>();
@@ -761,7 +751,7 @@ namespace Emby.Server.Implementations
                 ProcessFactory,
                 LocalizationManager,
                 () => SubtitleEncoder,
-                _configuration,
+                startupConfig,
                 StartupOptions.FFmpegPath);
             serviceCollection.AddSingleton(MediaEncoder);
 
@@ -783,7 +773,7 @@ namespace Emby.Server.Implementations
                 this,
                 LoggerFactory.CreateLogger<HttpListenerHost>(),
                 ServerConfigurationManager,
-                _configuration,
+                startupConfig,
                 NetworkManager,
                 JsonSerializer,
                 XmlSerializer,
@@ -846,7 +836,10 @@ namespace Emby.Server.Implementations
             UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager);
             serviceCollection.AddSingleton(UserViewManager);
 
-            NotificationManager = new NotificationManager(LoggerFactory, UserManager, ServerConfigurationManager);
+            NotificationManager = new NotificationManager(
+                LoggerFactory.CreateLogger<NotificationManager>(),
+                UserManager,
+                ServerConfigurationManager);
             serviceCollection.AddSingleton(NotificationManager);
 
             serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager));
@@ -1202,7 +1195,7 @@ namespace Emby.Server.Implementations
             });
         }
 
-        protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(Logger);
+        protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(LoggerFactory.CreateLogger<WebSocketSharpListener>());
 
         private CertificateInfo GetCertificateInfo(bool generateCertificate)
         {

+ 5 - 1
Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs

@@ -19,7 +19,11 @@ namespace Emby.Server.Implementations.Channels
         private readonly ILogger _logger;
         private readonly ILibraryManager _libraryManager;
 
-        public RefreshChannelsScheduledTask(IChannelManager channelManager, IUserManager userManager, ILogger logger, ILibraryManager libraryManager)
+        public RefreshChannelsScheduledTask(
+            IChannelManager channelManager,
+            IUserManager userManager,
+            ILogger<RefreshChannelsScheduledTask> logger,
+            ILibraryManager libraryManager)
         {
             _channelManager = channelManager;
             _userManager = userManager;

+ 4 - 1
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -347,7 +347,10 @@ namespace Emby.Server.Implementations.Collections
         private readonly IServerConfigurationManager _config;
         private readonly ILogger _logger;
 
-        public CollectionManagerEntryPoint(ICollectionManager collectionManager, IServerConfigurationManager config, ILogger logger)
+        public CollectionManagerEntryPoint(
+            ICollectionManager collectionManager,
+            IServerConfigurationManager config,
+            ILogger<CollectionManagerEntryPoint> logger)
         {
             _collectionManager = (CollectionManager)collectionManager;
             _config = config;

+ 0 - 1
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -8,7 +8,6 @@ namespace Emby.Server.Implementations
         public static Dictionary<string, string> Configuration => new Dictionary<string, string>
         {
             { "HttpListenerHost:DefaultRedirectPath", "web/index.html" },
-            { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" }
         };

+ 1 - 1
Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs

@@ -14,7 +14,7 @@ namespace Emby.Server.Implementations.Data
         private readonly ILibraryManager _libraryManager;
         private readonly ILogger _logger;
 
-        public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger logger)
+        public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
         {
             _libraryManager = libraryManager;
             _logger = logger;

+ 4 - 1
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -406,7 +406,10 @@ namespace Emby.Server.Implementations.Devices
         private readonly IServerConfigurationManager _config;
         private ILogger _logger;
 
-        public DeviceManagerEntryPoint(IDeviceManager deviceManager, IServerConfigurationManager config, ILogger logger)
+        public DeviceManagerEntryPoint(
+            IDeviceManager deviceManager,
+            IServerConfigurationManager config,
+            ILogger<DeviceManagerEntryPoint> logger)
         {
             _deviceManager = (DeviceManager)deviceManager;
             _config = config;

+ 6 - 1
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -55,7 +55,12 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private readonly IProviderManager _providerManager;
 
-        public LibraryChangedNotifier(ILibraryManager libraryManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger, IProviderManager providerManager)
+        public LibraryChangedNotifier(
+            ILibraryManager libraryManager,
+            ISessionManager sessionManager,
+            IUserManager userManager,
+            ILogger<LibraryChangedNotifier> logger,
+            IProviderManager providerManager)
         {
             _libraryManager = libraryManager;
             _sessionManager = sessionManager;

+ 21 - 19
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

@@ -12,14 +12,18 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
-    public class RecordingNotifier : IServerEntryPoint
+    public sealed class RecordingNotifier : IServerEntryPoint
     {
         private readonly ILiveTvManager _liveTvManager;
         private readonly ISessionManager _sessionManager;
         private readonly IUserManager _userManager;
         private readonly ILogger _logger;
 
-        public RecordingNotifier(ISessionManager sessionManager, IUserManager userManager, ILogger logger, ILiveTvManager liveTvManager)
+        public RecordingNotifier(
+            ISessionManager sessionManager,
+            IUserManager userManager,
+            ILogger<RecordingNotifier> logger,
+            ILiveTvManager liveTvManager)
         {
             _sessionManager = sessionManager;
             _userManager = userManager;
@@ -27,32 +31,33 @@ namespace Emby.Server.Implementations.EntryPoints
             _liveTvManager = liveTvManager;
         }
 
+        /// <inheritdoc />
         public Task RunAsync()
         {
-            _liveTvManager.TimerCancelled += _liveTvManager_TimerCancelled;
-            _liveTvManager.SeriesTimerCancelled += _liveTvManager_SeriesTimerCancelled;
-            _liveTvManager.TimerCreated += _liveTvManager_TimerCreated;
-            _liveTvManager.SeriesTimerCreated += _liveTvManager_SeriesTimerCreated;
+            _liveTvManager.TimerCancelled += OnLiveTvManagerTimerCancelled;
+            _liveTvManager.SeriesTimerCancelled += OnLiveTvManagerSeriesTimerCancelled;
+            _liveTvManager.TimerCreated += OnLiveTvManagerTimerCreated;
+            _liveTvManager.SeriesTimerCreated += OnLiveTvManagerSeriesTimerCreated;
 
             return Task.CompletedTask;
         }
 
-        private void _liveTvManager_SeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
             SendMessage("SeriesTimerCreated", e.Argument);
         }
 
-        private void _liveTvManager_TimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
             SendMessage("TimerCreated", e.Argument);
         }
 
-        private void _liveTvManager_SeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
             SendMessage("SeriesTimerCancelled", e.Argument);
         }
 
-        private void _liveTvManager_TimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+        private void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
         {
             SendMessage("TimerCancelled", e.Argument);
         }
@@ -63,11 +68,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
             try
             {
-                await _sessionManager.SendMessageToUserSessions(users, name, info, CancellationToken.None);
-            }
-            catch (ObjectDisposedException)
-            {
-                // TODO Log exception or Investigate and properly fix.
+                await _sessionManager.SendMessageToUserSessions(users, name, info, CancellationToken.None).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -75,12 +76,13 @@ namespace Emby.Server.Implementations.EntryPoints
             }
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
-            _liveTvManager.TimerCancelled -= _liveTvManager_TimerCancelled;
-            _liveTvManager.SeriesTimerCancelled -= _liveTvManager_SeriesTimerCancelled;
-            _liveTvManager.TimerCreated -= _liveTvManager_TimerCreated;
-            _liveTvManager.SeriesTimerCreated -= _liveTvManager_SeriesTimerCreated;
+            _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled;
+            _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled;
+            _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated;
+            _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated;
         }
     }
 }

+ 2 - 7
Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs

@@ -6,7 +6,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
@@ -15,21 +14,17 @@ namespace Emby.Server.Implementations.EntryPoints
     /// </summary>
     public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
     {
-        private readonly ILogger _logger;
-
         /// <summary>
         /// The user manager.
         /// </summary>
         private readonly IUserManager _userManager;
-
-        private IFileSystem _fileSystem;
+        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
         /// </summary>
-        public RefreshUsersMetadata(ILogger logger, IUserManager userManager, IFileSystem fileSystem)
+        public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem)
         {
-            _logger = logger;
             _userManager = userManager;
             _fileSystem = fileSystem;
         }

+ 3 - 12
Emby.Server.Implementations/EntryPoints/StartupWizard.cs

@@ -3,37 +3,28 @@ using Emby.Server.Implementations.Browser;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
     /// <summary>
     /// Class StartupWizard.
     /// </summary>
-    public class StartupWizard : IServerEntryPoint
+    public sealed class StartupWizard : IServerEntryPoint
     {
         /// <summary>
         /// The app host.
         /// </summary>
         private readonly IServerApplicationHost _appHost;
-
-        /// <summary>
-        /// The user manager.
-        /// </summary>
-        private readonly ILogger _logger;
-
-        private IServerConfigurationManager _config;
+        private readonly IServerConfigurationManager _config;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="StartupWizard"/> class.
         /// </summary>
         /// <param name="appHost">The application host.</param>
-        /// <param name="logger">The logger.</param>
         /// <param name="config">The configuration manager.</param>
-        public StartupWizard(IServerApplicationHost appHost, ILogger logger, IServerConfigurationManager config)
+        public StartupWizard(IServerApplicationHost appHost, IServerConfigurationManager config)
         {
             _appHost = appHost;
-            _logger = logger;
             _config = config;
         }
 

+ 1 - 5
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -3,8 +3,6 @@ using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
@@ -23,9 +21,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// The logger.
         /// </summary>
         private readonly ILogger _logger;
-        private readonly ISocketFactory _socketFactory;
         private readonly IServerApplicationHost _appHost;
-        private readonly IJsonSerializer _json;
 
         /// <summary>
         /// The UDP server.
@@ -64,7 +60,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
             _cancellationTokenSource.Cancel();
             _udpServer.Dispose();
-
+            _cancellationTokenSource.Dispose();
             _cancellationTokenSource = null;
             _udpServer = null;
 

+ 23 - 21
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

@@ -12,39 +12,38 @@ using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
-    public class UserDataChangeNotifier : IServerEntryPoint
+    public sealed class UserDataChangeNotifier : IServerEntryPoint
     {
+        private const int UpdateDuration = 500;
+
         private readonly ISessionManager _sessionManager;
-        private readonly ILogger _logger;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserManager _userManager;
 
+        private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
+
         private readonly object _syncLock = new object();
-        private Timer UpdateTimer { get; set; }
-        private const int UpdateDuration = 500;
+        private Timer _updateTimer;
 
-        private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
 
-        public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, ILogger logger, IUserManager userManager)
+        public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
         {
             _userDataManager = userDataManager;
             _sessionManager = sessionManager;
-            _logger = logger;
             _userManager = userManager;
         }
 
         public Task RunAsync()
         {
-            _userDataManager.UserDataSaved += _userDataManager_UserDataSaved;
+            _userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved;
 
             return Task.CompletedTask;
         }
 
-        void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e)
+        void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
         {
             if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
             {
@@ -53,14 +52,17 @@ namespace Emby.Server.Implementations.EntryPoints
 
             lock (_syncLock)
             {
-                if (UpdateTimer == null)
+                if (_updateTimer == null)
                 {
-                    UpdateTimer = new Timer(UpdateTimerCallback, null, UpdateDuration,
-                                                   Timeout.Infinite);
+                    _updateTimer = new Timer(
+                        UpdateTimerCallback,
+                        null,
+                        UpdateDuration,
+                        Timeout.Infinite);
                 }
                 else
                 {
-                    UpdateTimer.Change(UpdateDuration, Timeout.Infinite);
+                    _updateTimer.Change(UpdateDuration, Timeout.Infinite);
                 }
 
                 if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
@@ -96,10 +98,10 @@ namespace Emby.Server.Implementations.EntryPoints
 
                 var task = SendNotifications(changes, CancellationToken.None);
 
-                if (UpdateTimer != null)
+                if (_updateTimer != null)
                 {
-                    UpdateTimer.Dispose();
-                    UpdateTimer = null;
+                    _updateTimer.Dispose();
+                    _updateTimer = null;
                 }
             }
         }
@@ -144,13 +146,13 @@ namespace Emby.Server.Implementations.EntryPoints
 
         public void Dispose()
         {
-            if (UpdateTimer != null)
+            if (_updateTimer != null)
             {
-                UpdateTimer.Dispose();
-                UpdateTimer = null;
+                _updateTimer.Dispose();
+                _updateTimer = null;
             }
 
-            _userDataManager.UserDataSaved -= _userDataManager_UserDataSaved;
+            _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
         }
     }
 }

+ 1 - 1
Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs

@@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.HttpClientManager
             if (!string.IsNullOrWhiteSpace(userInfo))
             {
                 _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
-                url = url.Replace(userInfo + '@', string.Empty);
+                url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal);
             }
 
             var request = new HttpRequestMessage(method, url);

+ 15 - 12
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -39,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer
         private readonly Func<Type, Func<string, object>> _funcParseFn;
         private readonly string _defaultRedirectPath;
         private readonly string _baseUrlPrefix;
-        private readonly Dictionary<Type, Type> ServiceOperationsMap = new Dictionary<Type, Type>();
-        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
+        private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
         private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
+        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
         private bool _disposed = false;
 
         public HttpListenerHost(
@@ -71,6 +71,8 @@ namespace Emby.Server.Implementations.HttpServer
             ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
         }
 
+        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
         public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
 
         public static HttpListenerHost Instance { get; protected set; }
@@ -81,8 +83,6 @@ namespace Emby.Server.Implementations.HttpServer
 
         public ServiceController ServiceController { get; private set; }
 
-        public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
         public object CreateInstance(Type type)
         {
             return _appHost.CreateInstance(type);
@@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.HttpServer
 
         private static string NormalizeUrlPath(string path)
         {
-            if (path.StartsWith("/"))
+            if (path.Length > 0 && path[0] == '/')
             {
                 // If the path begins with a leading slash, just return it as-is
                 return path;
@@ -130,13 +130,13 @@ namespace Emby.Server.Implementations.HttpServer
 
         public Type GetServiceTypeByRequest(Type requestType)
         {
-            ServiceOperationsMap.TryGetValue(requestType, out var serviceType);
+            _serviceOperationsMap.TryGetValue(requestType, out var serviceType);
             return serviceType;
         }
 
         public void AddServiceInfo(Type serviceType, Type requestType)
         {
-            ServiceOperationsMap[requestType] = serviceType;
+            _serviceOperationsMap[requestType] = serviceType;
         }
 
         private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
@@ -198,7 +198,7 @@ namespace Emby.Server.Implementations.HttpServer
                 else
                 {
                     var inners = agg.InnerExceptions;
-                    if (inners != null && inners.Count > 0)
+                    if (inners.Count > 0)
                     {
                         return GetActualException(inners[0]);
                     }
@@ -361,7 +361,7 @@ namespace Emby.Server.Implementations.HttpServer
                 return true;
             }
 
-            host = host ?? string.Empty;
+            host ??= string.Empty;
 
             if (_networkManager.IsInPrivateAddressSpace(host))
             {
@@ -432,7 +432,7 @@ namespace Emby.Server.Implementations.HttpServer
         }
 
         /// <summary>
-        /// Overridable method that can be used to implement a custom hnandler
+        /// Overridable method that can be used to implement a custom handler.
         /// </summary>
         public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
         {
@@ -491,7 +491,7 @@ namespace Emby.Server.Implementations.HttpServer
                     || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
                     || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
                     || string.IsNullOrEmpty(localPath)
-                    || !localPath.StartsWith(_baseUrlPrefix))
+                    || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
                 {
                     // Always redirect back to the default path if the base prefix is invalid or missing
                     _logger.LogDebug("Normalizing a URL at {0}", localPath);
@@ -692,7 +692,10 @@ namespace Emby.Server.Implementations.HttpServer
 
         protected virtual void Dispose(bool disposing)
         {
-            if (_disposed) return;
+            if (_disposed)
+            {
+                return;
+            }
 
             if (disposing)
             {

+ 2 - 0
Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs

@@ -5,7 +5,9 @@ namespace Emby.Server.Implementations.IO
     public class ExtendedFileSystemInfo
     {
         public bool IsHidden { get; set; }
+
         public bool IsReadOnly { get; set; }
+
         public bool Exists { get; set; }
     }
 }

+ 21 - 19
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -14,27 +14,29 @@ namespace Emby.Server.Implementations.IO
 {
     public class FileRefresher : IDisposable
     {
-        private ILogger Logger { get; set; }
-        private ILibraryManager LibraryManager { get; set; }
-        private IServerConfigurationManager ConfigurationManager { get; set; }
+        private readonly ILogger _logger;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IServerConfigurationManager _configurationManager;
+
         private readonly List<string> _affectedPaths = new List<string>();
-        private Timer _timer;
         private readonly object _timerLock = new object();
-        public string Path { get; private set; }
-
-        public event EventHandler<EventArgs> Completed;
+        private Timer _timer;
 
         public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
         {
             logger.LogDebug("New file refresher created for {0}", path);
             Path = path;
 
-            ConfigurationManager = configurationManager;
-            LibraryManager = libraryManager;
-            Logger = logger;
+            _configurationManager = configurationManager;
+            _libraryManager = libraryManager;
+            _logger = logger;
             AddPath(path);
         }
 
+        public event EventHandler<EventArgs> Completed;
+
+        public string Path { get; private set; }
+
         private void AddAffectedPath(string path)
         {
             if (string.IsNullOrEmpty(path))
@@ -79,11 +81,11 @@ namespace Emby.Server.Implementations.IO
 
                 if (_timer == null)
                 {
-                    _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
+                    _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
                 }
                 else
                 {
-                    _timer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
+                    _timer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
                 }
             }
         }
@@ -92,7 +94,7 @@ namespace Emby.Server.Implementations.IO
         {
             lock (_timerLock)
             {
-                Logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
+                _logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
 
                 Path = path;
                 AddAffectedPath(path);
@@ -115,7 +117,7 @@ namespace Emby.Server.Implementations.IO
                 paths = _affectedPaths.ToList();
             }
 
-            Logger.LogDebug("Timer stopped.");
+            _logger.LogDebug("Timer stopped.");
 
             DisposeTimer();
             Completed?.Invoke(this, EventArgs.Empty);
@@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.IO
             }
             catch (Exception ex)
             {
-                Logger.LogError(ex, "Error processing directory changes");
+                _logger.LogError(ex, "Error processing directory changes");
             }
         }
 
@@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.IO
                     continue;
                 }
 
-                Logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
+                _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
 
                 try
                 {
@@ -157,11 +159,11 @@ namespace Emby.Server.Implementations.IO
                     // For now swallow and log.
                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
                     // Should we remove it from it's parent?
-                    Logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
                 }
                 catch (Exception ex)
                 {
-                    Logger.LogError(ex, "Error refreshing {name}", item.Name);
+                    _logger.LogError(ex, "Error refreshing {name}", item.Name);
                 }
             }
         }
@@ -177,7 +179,7 @@ namespace Emby.Server.Implementations.IO
 
             while (item == null && !string.IsNullOrEmpty(path))
             {
-                item = LibraryManager.FindByPath(path, null);
+                item = _libraryManager.FindByPath(path, null);
 
                 path = System.IO.Path.GetDirectoryName(path);
             }

+ 1 - 2
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -943,7 +943,6 @@ namespace Emby.Server.Implementations.Library
                     IncludeItemTypes = new[] { typeof(T).Name },
                     Name = name,
                     DtoOptions = options
-
                 }).Cast<MusicArtist>()
                 .OrderBy(i => i.IsAccessedByName ? 1 : 0)
                 .Cast<T>()
@@ -1079,7 +1078,7 @@ namespace Emby.Server.Implementations.Library
 
             var innerProgress = new ActionableProgress<double>();
 
-            innerProgress.RegisterAction(pct => progress.Report(pct * pct * 0.96));
+            innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
 
             // Validate the entire media library
             await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);

+ 5 - 1
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -27,7 +27,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <param name="fileSystem">The file system.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="config">The configuration manager.</param>
-        public MusicArtistResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config)
+        public MusicArtistResolver(
+            ILogger<MusicArtistResolver> logger,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager,
+            IServerConfigurationManager config)
         {
             _logger = logger;
             _fileSystem = fileSystem;

+ 33 - 27
Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs

@@ -5,60 +5,66 @@ using System.IO;
 using System.Linq;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.Model.Entities;
 
 namespace Emby.Server.Implementations.Library.Resolvers
 {
+    /// <summary>
+    /// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items.
+    /// </summary>
     public class PlaylistResolver : FolderResolver<Playlist>
     {
-        private string[] SupportedCollectionTypes = new string[] {
-
+        private string[] _musicPlaylistCollectionTypes = new string[] {
             string.Empty,
             CollectionType.Music
         };
 
-        /// <summary>
-        /// Resolves the specified args.
-        /// </summary>
-        /// <param name="args">The args.</param>
-        /// <returns>BoxSet.</returns>
+        /// <inheritdoc/>
         protected override Playlist Resolve(ItemResolveArgs args)
         {
-            // It's a boxset if all of the following conditions are met:
-            // Is a Directory
-            // Contains [playlist] in the path
             if (args.IsDirectory)
             {
-                var filename = Path.GetFileName(args.Path);
-
-                if (string.IsNullOrEmpty(filename))
+                // It's a boxset if the path is a directory with [playlist] in it's the name
+                // TODO: Should this use Path.GetDirectoryName() instead?
+                bool isBoxSet = Path.GetFileName(args.Path)
+                    ?.Contains("[playlist]", StringComparison.OrdinalIgnoreCase)
+                    ?? false;
+                if (isBoxSet)
                 {
-                    return null;
+                    return new Playlist
+                    {
+                        Path = args.Path,
+                        Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
+                    };
                 }
 
-                if (filename.IndexOf("[playlist]", StringComparison.OrdinalIgnoreCase) != -1)
+                // It's a directory-based playlist if the directory contains a playlist file
+                var filePaths = Directory.EnumerateFiles(args.Path);
+                if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
                 {
                     return new Playlist
                     {
                         Path = args.Path,
-                        Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
+                        Name = Path.GetFileName(args.Path)
                     };
                 }
             }
-            else
+
+            // Check if this is a music playlist file
+            // It should have the correct collection type and a supported file extension
+            else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
             {
-                if (SupportedCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                var extension = Path.GetExtension(args.Path);
+                if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
                 {
-                    var extension = Path.GetExtension(args.Path);
-                    if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                    return new Playlist
                     {
-                        return new Playlist
-                        {
-                            Path = args.Path,
-                            Name = Path.GetFileNameWithoutExtension(args.Path),
-                            IsInMixedFolder = true
-                        };
-                    }
+                        Path = args.Path,
+                        Name = Path.GetFileNameWithoutExtension(args.Path),
+                        IsInMixedFolder = true
+                    };
                 }
             }
 

+ 5 - 1
Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs

@@ -25,7 +25,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="localization">The localization</param>
         /// <param name="logger">The logger</param>
-        public SeasonResolver(IServerConfigurationManager config, ILibraryManager libraryManager, ILocalizationManager localization, ILogger logger)
+        public SeasonResolver(
+            IServerConfigurationManager config,
+            ILibraryManager libraryManager,
+            ILocalizationManager localization,
+            ILogger<SeasonResolver> logger)
         {
             _config = config;
             _libraryManager = libraryManager;

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         /// <param name="fileSystem">The file system.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="libraryManager">The library manager.</param>
-        public SeriesResolver(IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager)
+        public SeriesResolver(IFileSystem fileSystem, ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
         {
             _fileSystem = fileSystem;
             _logger = logger;

+ 4 - 1
Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs

@@ -25,7 +25,10 @@ namespace Emby.Server.Implementations.Library.Validators
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="itemRepo">The item repository.</param>
-        public ArtistsPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+        public ArtistsPostScanTask(
+            ILibraryManager libraryManager,
+            ILogger<ArtistsPostScanTask> logger,
+            IItemRepository itemRepo)
         {
             _libraryManager = libraryManager;
             _logger = logger;

+ 4 - 1
Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs

@@ -25,7 +25,10 @@ namespace Emby.Server.Implementations.Library.Validators
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="itemRepo">The item repository.</param>
-        public GenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+        public GenresPostScanTask(
+            ILibraryManager libraryManager,
+            ILogger<GenresPostScanTask> logger,
+            IItemRepository itemRepo)
         {
             _libraryManager = libraryManager;
             _logger = logger;

+ 4 - 1
Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs

@@ -25,7 +25,10 @@ namespace Emby.Server.Implementations.Library.Validators
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="itemRepo">The item repository.</param>
-        public MusicGenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+        public MusicGenresPostScanTask(
+            ILibraryManager libraryManager,
+            ILogger<MusicGenresPostScanTask> logger,
+            IItemRepository itemRepo)
         {
             _libraryManager = libraryManager;
             _logger = logger;

+ 4 - 1
Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs

@@ -26,7 +26,10 @@ namespace Emby.Server.Implementations.Library.Validators
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="itemRepo">The item repository.</param>
-        public StudiosPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+        public StudiosPostScanTask(
+            ILibraryManager libraryManager,
+            ILogger<StudiosPostScanTask> logger,
+            IItemRepository itemRepo)
         {
             _libraryManager = libraryManager;
             _logger = logger;

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.IO;
 using System.Net.Http;

+ 1 - 2
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -29,7 +29,6 @@ using MediaBrowser.Model.Diagnostics;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
@@ -80,7 +79,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             IServerApplicationHost appHost,
             IStreamHelper streamHelper,
             IMediaSourceManager mediaSourceManager,
-            ILogger logger,
+            ILogger<EmbyTV> logger,
             IJsonSerializer jsonSerializer,
             IHttpClient httpClient,
             IServerConfigurationManager config,

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;

+ 5 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Plugins;
 
@@ -5,11 +8,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 {
     public class EntryPoint : IServerEntryPoint
     {
+        /// <inheritdoc />
         public Task RunAsync()
         {
             return EmbyTV.Current.Start();
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
         }

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Threading;
 using System.Threading.Tasks;

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.IO;

+ 13 - 5
Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Globalization;
 using MediaBrowser.Controller.LiveTv;
@@ -21,7 +24,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue)
                 {
-                    name += string.Format(" S{0}E{1}", info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture), info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
+                    name += string.Format(
+                        CultureInfo.InvariantCulture,
+                        " S{0}E{1}",
+                        info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture),
+                        info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
                     addHyphen = false;
                 }
                 else if (info.OriginalAirDate.HasValue)
@@ -32,7 +39,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     }
                     else
                     {
-                        name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd");
+                        name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
                     }
                 }
                 else
@@ -67,14 +74,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             date = date.ToLocalTime();
 
-            return string.Format("{0}_{1}_{2}_{3}_{4}_{5}",
+            return string.Format(
+                CultureInfo.InvariantCulture,
+                "{0}_{1}_{2}_{3}_{4}_{5}",
                 date.Year.ToString("0000", CultureInfo.InvariantCulture),
                 date.Month.ToString("00", CultureInfo.InvariantCulture),
                 date.Day.ToString("00", CultureInfo.InvariantCulture),
                 date.Hour.ToString("00", CultureInfo.InvariantCulture),
                 date.Minute.ToString("00", CultureInfo.InvariantCulture),
-                date.Second.ToString("00", CultureInfo.InvariantCulture)
-                );
+                date.Second.ToString("00", CultureInfo.InvariantCulture));
         }
     }
 }

+ 4 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Serialization;
@@ -12,6 +15,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
         }
 
+        /// <inheritdoc />
         public override void Add(SeriesTimerInfo item)
         {
             if (string.IsNullOrEmpty(item.Id))

+ 3 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Concurrent;
 using System.Globalization;

+ 8 - 1
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -30,7 +33,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
         private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
 
-        public SchedulesDirect(ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IApplicationHost appHost)
+        public SchedulesDirect(
+            ILogger<SchedulesDirect> logger,
+            IJsonSerializer jsonSerializer,
+            IHttpClient httpClient,
+            IApplicationHost appHost)
         {
             _logger = logger;
             _jsonSerializer = jsonSerializer;

+ 6 - 3
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -31,7 +34,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         public XmlTvListingsProvider(
             IServerConfigurationManager config,
             IHttpClient httpClient,
-            ILogger logger,
+            ILogger<XmlTvListingsProvider> logger,
             IFileSystem fileSystem,
             IZipClient zipClient)
         {
@@ -91,12 +94,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 {
                     using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
                     {
-                        await gzStream.CopyToAsync(fileStream).ConfigureAwait(false);
+                        await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
                     }
                 }
                 else
                 {
-                    await stream.CopyToAsync(fileStream).ConfigureAwait(false);
+                    await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
                 }
             }
 

+ 3 - 0
Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System.Collections.Generic;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.LiveTv;

+ 3 - 0
Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Globalization;
 using System.Linq;

+ 1 - 0
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1,4 +1,5 @@
 #pragma warning disable CS1591
+#pragma warning disable SA1600
 
 using System;
 using System.Collections.Generic;

+ 22 - 26
Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -1,52 +1,48 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.LiveTv
 {
     public class LiveTvMediaSourceProvider : IMediaSourceProvider
     {
+        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
+        private const char StreamIdDelimeter = '_';
+        private const string StreamIdDelimeterString = "_";
+
         private readonly ILiveTvManager _liveTvManager;
-        private readonly IJsonSerializer _jsonSerializer;
         private readonly ILogger _logger;
         private readonly IMediaSourceManager _mediaSourceManager;
-        private readonly IMediaEncoder _mediaEncoder;
         private readonly IServerApplicationHost _appHost;
-        private IApplicationPaths _appPaths;
 
-        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost)
+        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, ILogger<LiveTvMediaSourceProvider> logger, IMediaSourceManager mediaSourceManager, IServerApplicationHost appHost)
         {
             _liveTvManager = liveTvManager;
-            _jsonSerializer = jsonSerializer;
+            _logger = logger;
             _mediaSourceManager = mediaSourceManager;
-            _mediaEncoder = mediaEncoder;
             _appHost = appHost;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
-            _appPaths = appPaths;
         }
 
         public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
         {
-            var baseItem = (BaseItem)item;
-
-            if (baseItem.SourceType == SourceType.LiveTV)
+            if (item.SourceType == SourceType.LiveTV)
             {
                 var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path);
 
-                if (string.IsNullOrEmpty(baseItem.Path) || activeRecordingInfo != null)
+                if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo != null)
                 {
                     return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken);
                 }
@@ -55,10 +51,6 @@ namespace Emby.Server.Implementations.LiveTv
             return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
         }
 
-        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
-        private const char StreamIdDelimeter = '_';
-        private const string StreamIdDelimeterString = "_";
-
         private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
         {
             IEnumerable<MediaSourceInfo> sources;
@@ -91,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv
             foreach (var source in list)
             {
                 source.Type = MediaSourceType.Default;
-                source.BufferMs = source.BufferMs ?? 1500;
+                source.BufferMs ??= 1500;
 
                 if (source.RequiresOpening || forceRequireOpening)
                 {
@@ -100,11 +92,14 @@ namespace Emby.Server.Implementations.LiveTv
 
                 if (source.RequiresOpening)
                 {
-                    var openKeys = new List<string>();
-                    openKeys.Add(item.GetType().Name);
-                    openKeys.Add(item.Id.ToString("N", CultureInfo.InvariantCulture));
-                    openKeys.Add(source.Id ?? string.Empty);
-                    source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray());
+                    var openKeys = new List<string>
+                    {
+                        item.GetType().Name,
+                        item.Id.ToString("N", CultureInfo.InvariantCulture),
+                        source.Id ?? string.Empty
+                    };
+
+                    source.OpenToken = string.Join(StreamIdDelimeterString, openKeys);
                 }
 
                 // Dummy this up so that direct play checks can still run
@@ -114,11 +109,12 @@ namespace Emby.Server.Implementations.LiveTv
                 }
             }
 
-            _logger.LogDebug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
+            _logger.LogDebug("MediaSources: {@MediaSources}", list);
 
             return list;
         }
 
+        /// <inheritdoc />
         public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
         {
             var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;

+ 4 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -36,7 +39,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
         public HdHomerunHost(
             IServerConfigurationManager config,
-            ILogger logger,
+            ILogger<HdHomerunHost> logger,
             IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
             IHttpClient httpClient,

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Buffers;
 using System.Collections.Generic;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.IO;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;

+ 3 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
 using System;
 using System.Collections.Generic;
 using System.IO;

+ 13 - 13
Emby.Server.Implementations/Localization/Core/ar.json

@@ -2,22 +2,22 @@
     "Albums": "ألبومات",
     "AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
     "Application": "التطبيق",
-    "Artists": "الفنان",
+    "Artists": "الفنانين",
     "AuthenticationSucceededWithUserName": "{0} سجل الدخول بنجاح",
     "Books": "كتب",
     "CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}",
     "Channels": "القنوات",
-    "ChapterNameValue": "الباب {0}",
+    "ChapterNameValue": "فصل {0}",
     "Collections": "مجموعات",
-    "DeviceOfflineWithName": "تم قطع اتصال {0}",
+    "DeviceOfflineWithName": "قُطِع الاتصال بـ{0}",
     "DeviceOnlineWithName": "{0} متصل",
     "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
-    "Favorites": "التفضيلات",
+    "Favorites": "المفضلة",
     "Folders": "المجلدات",
-    "Genres": "أنواع الأفلام",
+    "Genres": "الأنواع",
     "HeaderAlbumArtists": "فناني الألبومات",
     "HeaderCameraUploads": "تحميلات الكاميرا",
-    "HeaderContinueWatching": "استئناف المشاهدة",
+    "HeaderContinueWatching": "استئناف",
     "HeaderFavoriteAlbums": "الألبومات المفضلة",
     "HeaderFavoriteArtists": "الفنانون المفضلون",
     "HeaderFavoriteEpisodes": "الحلقات المفضلة",
@@ -31,28 +31,28 @@
     "ItemAddedWithName": "تم إضافة {0} للمكتبة",
     "ItemRemovedWithName": "تم إزالة {0} من المكتبة",
     "LabelIpAddressValue": "عنوان الآي بي: {0}",
-    "LabelRunningTimeValue": "وقت التشغيل: {0}",
+    "LabelRunningTimeValue": "المدة: {0}",
     "Latest": "الأحدث",
-    "MessageApplicationUpdated": "لقد تم تحديث خادم أمبي",
+    "MessageApplicationUpdated": "لقد تم تحديث خادم Jellyfin",
     "MessageApplicationUpdatedTo": "تم تحديث سيرفر Jellyfin الى {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث إعدادات الخادم في قسم {0}",
     "MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم",
-    "MixedContent": "محتوى مخلوط",
+    "MixedContent": "محتوى مختلط",
     "Movies": "الأفلام",
     "Music": "الموسيقى",
     "MusicVideos": "الفيديوهات الموسيقية",
     "NameInstallFailed": "فشل التثبيت {0}",
     "NameSeasonNumber": "الموسم {0}",
     "NameSeasonUnknown": "الموسم غير معروف",
-    "NewVersionIsAvailable": "نسخة حديثة من سيرفر Jellyfin متوفرة للتحميل .",
+    "NewVersionIsAvailable": "نسخة جديدة من سيرفر Jellyfin متوفرة للتحميل.",
     "NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق",
     "NotificationOptionApplicationUpdateInstalled": "تم تحديث التطبيق",
     "NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
     "NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي",
-    "NotificationOptionCameraImageUploaded": "تم رقع صورة الكاميرا",
+    "NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا",
     "NotificationOptionInstallationFailed": "فشل في التثبيت",
-    "NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد",
-    "NotificationOptionPluginError": "فشل في الملحق",
+    "NotificationOptionNewLibraryContent": "أُضِيفَ محتوى جديد",
+    "NotificationOptionPluginError": "فشل في الـPlugin",
     "NotificationOptionPluginInstalled": "تم تثبيت الملحق",
     "NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
     "NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/da.json

@@ -3,7 +3,7 @@
     "AppDeviceValues": "App: {0}, Enhed: {1}",
     "Application": "Applikation",
     "Artists": "Kunstnere",
-    "AuthenticationSucceededWithUserName": "{0} bekræftet med succes",
+    "AuthenticationSucceededWithUserName": "{0} succesfuldt autentificeret",
     "Books": "Bøger",
     "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
     "Channels": "Kanaler",

+ 5 - 5
Emby.Server.Implementations/Localization/Core/de.json

@@ -1,15 +1,15 @@
 {
     "Albums": "Alben",
-    "AppDeviceValues": "App: {0}, Gerät: {1}",
-    "Application": "Anwendung",
+    "AppDeviceValues": "Anw: {0}, Gerät: {1}",
+    "Application": "Programm",
     "Artists": "Interpreten",
     "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
     "Books": "Bücher",
-    "CameraImageUploadedFrom": "Ein neues Foto wurde hochgeladen von {0}",
+    "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
     "Channels": "Kanäle",
     "ChapterNameValue": "Kapitel {0}",
     "Collections": "Sammlungen",
-    "DeviceOfflineWithName": "{0} wurde getrennt",
+    "DeviceOfflineWithName": "{0} hat die Verbindung getrennt",
     "DeviceOnlineWithName": "{0} ist verbunden",
     "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
     "Favorites": "Favoriten",
@@ -17,7 +17,7 @@
     "Genres": "Genres",
     "HeaderAlbumArtists": "Album-Interpreten",
     "HeaderCameraUploads": "Kamera-Uploads",
-    "HeaderContinueWatching": "Weiterschauen",
+    "HeaderContinueWatching": "Fortsetzen",
     "HeaderFavoriteAlbums": "Lieblingsalben",
     "HeaderFavoriteArtists": "Lieblings-Interpreten",
     "HeaderFavoriteEpisodes": "Lieblingsepisoden",

+ 8 - 1
Emby.Server.Implementations/Localization/Core/es_DO.json

@@ -10,5 +10,12 @@
     "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
     "AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
     "Application": "Aplicación",
-    "AppDeviceValues": "App: {0}, Dispositivo: {1}"
+    "AppDeviceValues": "App: {0}, Dispositivo: {1}",
+    "HeaderContinueWatching": "Continuar Viendo",
+    "HeaderCameraUploads": "Subidas de Cámara",
+    "HeaderAlbumArtists": "Artistas del Álbum",
+    "Genres": "Géneros",
+    "Folders": "Carpetas",
+    "Favorites": "Favoritos",
+    "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}"
 }

+ 20 - 20
Emby.Server.Implementations/Localization/Core/fi.json

@@ -17,31 +17,31 @@
     "LabelIpAddressValue": "IP-osoite: {0}",
     "ItemRemovedWithName": "{0} poistettiin kirjastosta",
     "ItemAddedWithName": "{0} lisättiin kirjastoon",
-    "Inherit": "Periä",
+    "Inherit": "Periytyä",
     "HomeVideos": "Kotivideot",
-    "HeaderRecordingGroups": "Äänitysryhmät",
+    "HeaderRecordingGroups": "Nauhoitusryhmät",
     "HeaderNextUp": "Seuraavaksi",
     "HeaderFavoriteSongs": "Lempikappaleet",
     "HeaderFavoriteShows": "Lempisarjat",
     "HeaderFavoriteEpisodes": "Lempijaksot",
-    "HeaderCameraUploads": "Kamerasta lähetetyt",
+    "HeaderCameraUploads": "Kameralataukset",
     "HeaderFavoriteArtists": "Lempiartistit",
     "HeaderFavoriteAlbums": "Lempialbumit",
     "HeaderContinueWatching": "Jatka katsomista",
-    "HeaderAlbumArtists": "Albumin artistit",
-    "Genres": "Tyylilaji",
+    "HeaderAlbumArtists": "Albumin esittäjä",
+    "Genres": "Tyylilajit",
     "Folders": "Kansiot",
     "Favorites": "Suosikit",
-    "FailedLoginAttemptWithUserName": "Epäonnistunut kirjautumisyritys kohteesta {0}",
-    "DeviceOnlineWithName": "{0} on yhdistynyt",
+    "FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
+    "DeviceOnlineWithName": "{0} on yhdistetty",
     "DeviceOfflineWithName": "{0} on katkaissut yhteytensä",
     "Collections": "Kokoelmat",
     "ChapterNameValue": "Luku: {0}",
     "Channels": "Kanavat",
-    "CameraImageUploadedFrom": "Uusi kamerakuva on lähetetty kohteesta {0}",
+    "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
     "Books": "Kirjat",
-    "AuthenticationSucceededWithUserName": "{0} todennettu onnistuneesti",
-    "Artists": "Artistit",
+    "AuthenticationSucceededWithUserName": "{0} todennus onnistui",
+    "Artists": "Esiintyjät",
     "Application": "Sovellus",
     "AppDeviceValues": "Sovellus: {0}, Laite: {1}",
     "Albums": "Albumit",
@@ -76,21 +76,21 @@
     "Shows": "Ohjelmat",
     "ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen",
     "ProviderValue": "Palveluntarjoaja: {0}",
-    "Plugin": "Laajennus",
+    "Plugin": "Liitännäinen",
     "NotificationOptionVideoPlaybackStopped": "Videon toistaminen pysäytetty",
     "NotificationOptionVideoPlayback": "Videon toistaminen aloitettu",
     "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
     "NotificationOptionTaskFailed": "Ajastetun tehtävän ongelma",
     "NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan",
-    "NotificationOptionPluginUpdateInstalled": "Laajennuksen päivitys asennettu",
-    "NotificationOptionPluginUninstalled": "Laajennus poistettu",
-    "NotificationOptionPluginInstalled": "Laajennus asennettu",
-    "NotificationOptionPluginError": "Ongelma laajennuksessa",
-    "NotificationOptionNewLibraryContent": "Uusi sisältö lisätty",
-    "NotificationOptionInstallationFailed": "Asennusvirhe",
-    "NotificationOptionCameraImageUploaded": "Kameran kuva lisätty",
-    "NotificationOptionAudioPlaybackStopped": "Äänen toistaminen pysäytetty",
-    "NotificationOptionAudioPlayback": "Äänen toistaminen aloitettu",
+    "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
+    "NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
+    "NotificationOptionPluginInstalled": "Liitännäinen asennettu",
+    "NotificationOptionPluginError": "Ongelma liitännäisessä",
+    "NotificationOptionNewLibraryContent": "Uutta sisältöä lisätty",
+    "NotificationOptionInstallationFailed": "Asennus epäonnistui",
+    "NotificationOptionCameraImageUploaded": "Kuva ladattu kamerasta",
+    "NotificationOptionAudioPlaybackStopped": "Audion toisto pysäytetty",
+    "NotificationOptionAudioPlayback": "Audion toisto aloitettu",
     "NotificationOptionApplicationUpdateInstalled": "Ohjelmistopäivitys asennettu",
     "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla"
 }

+ 30 - 30
Emby.Server.Implementations/Localization/Core/he.json

@@ -2,33 +2,33 @@
     "Albums": "אלבומים",
     "AppDeviceValues": "יישום: {0}, מכשיר: {1}",
     "Application": "אפליקציה",
-    "Artists": "אמנים",
-    "AuthenticationSucceededWithUserName": "{0} זוהה בהצלחה",
+    "Artists": "אומנים",
+    "AuthenticationSucceededWithUserName": "{0} אומת בהצלחה",
     "Books": "ספרים",
-    "CameraImageUploadedFrom": "תמונה חדשה הועלתה מ{0}",
+    "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מ {0}",
     "Channels": "ערוצים",
     "ChapterNameValue": "פרק {0}",
-    "Collections": "קולקציות",
+    "Collections": "אוספים",
     "DeviceOfflineWithName": "{0} התנתק",
     "DeviceOnlineWithName": "{0} מחובר",
     "FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי מ{0}",
-    "Favorites": "אהובים",
+    "Favorites": "מועדפים",
     "Folders": "תיקיות",
     "Genres": "ז'אנרים",
     "HeaderAlbumArtists": "אמני האלבום",
     "HeaderCameraUploads": "העלאות ממצלמה",
     "HeaderContinueWatching": "המשך לצפות",
     "HeaderFavoriteAlbums": "אלבומים שאהבתי",
-    "HeaderFavoriteArtists": "אמנים שאהבתי",
-    "HeaderFavoriteEpisodes": "פרקים אהובים",
-    "HeaderFavoriteShows": "תוכניות אהובות",
-    "HeaderFavoriteSongs": "שירים שאהבתי",
-    "HeaderLiveTV": "טלוויזיה בשידור חי",
+    "HeaderFavoriteArtists": "אמנים מועדפים",
+    "HeaderFavoriteEpisodes": "פרקים מועדפים",
+    "HeaderFavoriteShows": "סדרות מועדפות",
+    "HeaderFavoriteSongs": "שירים מועדפים",
+    "HeaderLiveTV": "שידורים חיים",
     "HeaderNextUp": "הבא",
     "HeaderRecordingGroups": "קבוצות הקלטה",
     "HomeVideos": "סרטונים בייתים",
     "Inherit": "הורש",
-    "ItemAddedWithName": "{0} was added to the library",
+    "ItemAddedWithName": "{0} הוסף לספרייה",
     "ItemRemovedWithName": "{0} נמחק מהספרייה",
     "LabelIpAddressValue": "Ip כתובת: {0}",
     "LabelRunningTimeValue": "משך צפייה: {0}",
@@ -36,15 +36,15 @@
     "MessageApplicationUpdated": "שרת הJellyfin עודכן",
     "MessageApplicationUpdatedTo": "שרת הJellyfin עודכן לגרסא {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "הגדרת השרת {0} שונתה",
-    "MessageServerConfigurationUpdated": "Server configuration has been updated",
+    "MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
     "MixedContent": "תוכן מעורב",
     "Movies": "סרטים",
     "Music": "מוזיקה",
-    "MusicVideos": "Music videos",
-    "NameInstallFailed": "{0} installation failed",
-    "NameSeasonNumber": "Season {0}",
-    "NameSeasonUnknown": "Season Unknown",
-    "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
+    "MusicVideos": "קליפים",
+    "NameInstallFailed": "התקנת {0} נכשלה",
+    "NameSeasonNumber": "עונה {0}",
+    "NameSeasonUnknown": "עונה לא ידועה",
+    "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
     "NotificationOptionApplicationUpdateAvailable": "Application update available",
     "NotificationOptionApplicationUpdateInstalled": "Application update installed",
     "NotificationOptionAudioPlayback": "Audio playback started",
@@ -53,10 +53,10 @@
     "NotificationOptionInstallationFailed": "התקנה נכשלה",
     "NotificationOptionNewLibraryContent": "New content added",
     "NotificationOptionPluginError": "Plugin failure",
-    "NotificationOptionPluginInstalled": "Plugin installed",
-    "NotificationOptionPluginUninstalled": "Plugin uninstalled",
-    "NotificationOptionPluginUpdateInstalled": "Plugin update installed",
-    "NotificationOptionServerRestartRequired": "Server restart required",
+    "NotificationOptionPluginInstalled": "התוסף הותקן",
+    "NotificationOptionPluginUninstalled": "התוסף הוסר",
+    "NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
+    "NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
     "NotificationOptionTaskFailed": "Scheduled task failure",
     "NotificationOptionUserLockedOut": "User locked out",
     "NotificationOptionVideoPlayback": "Video playback started",
@@ -71,26 +71,26 @@
     "ScheduledTaskFailedWithName": "{0} failed",
     "ScheduledTaskStartedWithName": "{0} started",
     "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
-    "Shows": "Shows",
-    "Songs": "Songs",
-    "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
+    "Shows": "סדרות",
+    "Songs": "שירים",
+    "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
     "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
     "Sync": "סנכרן",
     "System": "System",
-    "TvShows": "TV Shows",
+    "TvShows": "סדרות טלוויזיה",
     "User": "User",
-    "UserCreatedWithName": "User {0} has been created",
-    "UserDeletedWithName": "User {0} has been deleted",
-    "UserDownloadingItemWithValues": "{0} is downloading {1}",
+    "UserCreatedWithName": "המשתמש {0} נוצר",
+    "UserDeletedWithName": "המשתמש {0} הוסר",
+    "UserDownloadingItemWithValues": "{0} מוריד את {1}",
     "UserLockedOutWithName": "User {0} has been locked out",
     "UserOfflineFromDevice": "{0} has disconnected from {1}",
     "UserOnlineFromDevice": "{0} is online from {1}",
     "UserPasswordChangedWithName": "Password has been changed for user {0}",
     "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
-    "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
-    "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
+    "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
+    "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
     "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
     "ValueSpecialEpisodeName": "מיוחד- {0}",
     "VersionNumber": "Version {0}"

+ 5 - 5
Emby.Server.Implementations/Localization/Core/hu.json

@@ -11,7 +11,7 @@
     "Collections": "Gyűjtemények",
     "DeviceOfflineWithName": "{0} kijelentkezett",
     "DeviceOnlineWithName": "{0} belépett",
-    "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet {0}",
+    "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet tőle: {0}",
     "Favorites": "Kedvencek",
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
@@ -27,7 +27,7 @@
     "HeaderNextUp": "Következik",
     "HeaderRecordingGroups": "Felvételi csoportok",
     "HomeVideos": "Házi videók",
-    "Inherit": "Öröklés",
+    "Inherit": "Örökölt",
     "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
     "ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
     "LabelIpAddressValue": "IP cím: {0}",
@@ -73,7 +73,7 @@
     "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani",
     "Shows": "Műsorok",
     "Songs": "Dalok",
-    "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek próbáld újra később.",
+    "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen:  {0} ehhez: {1}",
     "SubtitlesDownloadedForItem": "Letöltött feliratok a következőhöz: {0}",
@@ -86,11 +86,11 @@
     "UserDownloadingItemWithValues": "{0} letölti {1}",
     "UserLockedOutWithName": "{0}  felhasználó zárolva van",
     "UserOfflineFromDevice": "{0} kijelentkezett innen:  {1}",
-    "UserOnlineFromDevice": "{0} online itt:  {1}",
+    "UserOnlineFromDevice": "{0} online innen: {1}",
     "UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}",
     "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}",
     "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt:  {2}",
-    "UserStoppedPlayingItemWithValues": "{0} befejezte a következőt: {1} itt:  {2}",
+    "UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}",
     "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
     "ValueSpecialEpisodeName": "Special - {0}",
     "VersionNumber": "Verzió: {0}"

+ 2 - 1
Emby.Server.Implementations/Localization/Core/id.json

@@ -91,5 +91,6 @@
     "NotificationOptionVideoPlayback": "Pemutaran video dimulai",
     "NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
     "NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
-    "MixedContent": "Konten campur"
+    "MixedContent": "Konten campur",
+    "PluginUninstalledWithName": "{0} telah dihapus"
 }

+ 2 - 2
Emby.Server.Implementations/Localization/Core/it.json

@@ -5,7 +5,7 @@
     "Artists": "Artisti",
     "AuthenticationSucceededWithUserName": "{0} autenticato con successo",
     "Books": "Libri",
-    "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera {0}",
+    "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera da {0}",
     "Channels": "Canali",
     "ChapterNameValue": "Capitolo {0}",
     "Collections": "Collezioni",
@@ -18,7 +18,7 @@
     "HeaderAlbumArtists": "Artisti dell' Album",
     "HeaderCameraUploads": "Caricamenti Fotocamera",
     "HeaderContinueWatching": "Continua a guardare",
-    "HeaderFavoriteAlbums": "Album preferiti",
+    "HeaderFavoriteAlbums": "Album Preferiti",
     "HeaderFavoriteArtists": "Artisti Preferiti",
     "HeaderFavoriteEpisodes": "Episodi Preferiti",
     "HeaderFavoriteShows": "Serie TV Preferite",

+ 96 - 0
Emby.Server.Implementations/Localization/Core/lv.json

@@ -0,0 +1,96 @@
+{
+    "ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
+    "NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
+    "HeaderRecordingGroups": "Ierakstu Grupas",
+    "UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
+    "SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
+    "NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
+    "NotificationOptionVideoPlayback": "Video atskaņošana sākta",
+    "NotificationOptionInstallationFailed": "Instalācija neizdevās",
+    "AuthenticationSucceededWithUserName": "{0} veiksmīgi autentificējies",
+    "ValueSpecialEpisodeName": "Speciālais - {0}",
+    "ScheduledTaskStartedWithName": "{0} iesākts",
+    "ScheduledTaskFailedWithName": "{0} neizdevās",
+    "Photos": "Attēli",
+    "NotificationOptionUserLockedOut": "Lietotājs bloķēts",
+    "LabelRunningTimeValue": "Garums: {0}",
+    "Inherit": "Mantot",
+    "AppDeviceValues": "Lietotne:{0}, Ierīce:{1}",
+    "VersionNumber": "Versija {0}",
+    "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots tavai multvides bibliotēkai",
+    "UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
+    "UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
+    "UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
+    "UserOnlineFromDevice": "{0} ir tiešsaistē no {1}",
+    "UserOfflineFromDevice": "{0} ir atvienojies no {1}",
+    "UserLockedOutWithName": "Lietotājs {0} ir ticis bloķēts",
+    "UserDownloadingItemWithValues": "{0} lejupielādē {1}",
+    "UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
+    "UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
+    "User": "Lietotājs",
+    "TvShows": "TV Raidījumi",
+    "Sync": "Sinhronizācija",
+    "System": "Sistēma",
+    "SubtitlesDownloadedForItem": "Subtitri lejupielādēti priekš {0}",
+    "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
+    "Songs": "Dziesmas",
+    "Shows": "Raidījumi",
+    "PluginUpdatedWithName": "{0} tika atjaunots",
+    "PluginUninstalledWithName": "{0} tika noņemts",
+    "PluginInstalledWithName": "{0} tika uzstādīts",
+    "Plugin": "Paplašinājums",
+    "Playlists": "Atskaņošanas Saraksti",
+    "MixedContent": "Jaukts saturs",
+    "HomeVideos": "Mājas Video",
+    "HeaderNextUp": "Nākamais",
+    "ChapterNameValue": "Nodaļa {0}",
+    "Application": "Lietotne",
+    "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
+    "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
+    "NotificationOptionPluginUninstalled": "Paplašinājums noņemts",
+    "NotificationOptionPluginInstalled": "Paplašinājums uzstādīts",
+    "NotificationOptionPluginError": "Paplašinājuma kļūda",
+    "NotificationOptionNewLibraryContent": "Jauns saturs pievienots",
+    "NotificationOptionCameraImageUploaded": "Kameras attēls augšupielādēts",
+    "NotificationOptionAudioPlaybackStopped": "Audio atskaņošana apturēta",
+    "NotificationOptionAudioPlayback": "Audio atskaņošana sākta",
+    "NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
+    "NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
+    "NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
+    "NameSeasonUnknown": "Nezināma Sezona",
+    "NameSeasonNumber": "Sezona {0}",
+    "NameInstallFailed": "{0} instalācija neizdevās",
+    "MusicVideos": "Mūzikas video",
+    "Music": "Mūzika",
+    "Movies": "Filmas",
+    "MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota",
+    "MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
+    "MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
+    "Latest": "Jaunākais",
+    "LabelIpAddressValue": "IP adrese: {0}",
+    "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
+    "ItemAddedWithName": "{0} tika pievienots bibliotēkai",
+    "HeaderLiveTV": "Tiešraides TV",
+    "HeaderContinueWatching": "Turpināt Skatīšanos",
+    "HeaderCameraUploads": "Kameras augšupielādes",
+    "HeaderAlbumArtists": "Albumu Izpildītāji",
+    "Genres": "Žanri",
+    "Folders": "Mapes",
+    "Favorites": "Favorīti",
+    "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}",
+    "DeviceOnlineWithName": "{0} ir pievienojies",
+    "DeviceOfflineWithName": "{0} ir atvienojies",
+    "Collections": "Kolekcijas",
+    "Channels": "Kanāli",
+    "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
+    "Books": "Grāmatas",
+    "Artists": "Izpildītāji",
+    "Albums": "Albumi",
+    "ProviderValue": "Provider: {0}",
+    "HeaderFavoriteSongs": "Dziesmu Favorīti",
+    "HeaderFavoriteShows": "Raidījumu Favorīti",
+    "HeaderFavoriteEpisodes": "Episožu Favorīti",
+    "HeaderFavoriteArtists": "Izpildītāju Favorīti",
+    "HeaderFavoriteAlbums": "Albumu Favorīti"
+}

+ 96 - 0
Emby.Server.Implementations/Localization/Core/mk.json

@@ -0,0 +1,96 @@
+{
+    "ScheduledTaskFailedWithName": "{0} неуспешно",
+    "ProviderValue": "Провајдер: {0}",
+    "PluginUpdatedWithName": "{0} беше надоградено",
+    "PluginUninstalledWithName": "{0} беше успешно деинсталирано",
+    "PluginInstalledWithName": "{0} беше успешно инсталирано",
+    "Plugin": "Додатоци",
+    "Playlists": "Листи",
+    "Photos": "Слики",
+    "NotificationOptionVideoPlaybackStopped": "Видео стопирано",
+    "NotificationOptionVideoPlayback": "Видео пуштено",
+    "NotificationOptionUserLockedOut": "Корисникот е ослободен",
+    "NotificationOptionTaskFailed": "Закажани задачи неуспешно",
+    "NotificationOptionServerRestartRequired": "Задолжително рестартирање на серверот",
+    "NotificationOptionPluginUpdateInstalled": "Надоградба на Додаток успешна",
+    "NotificationOptionPluginUninstalled": "Додаток успешно деинсталиран",
+    "NotificationOptionPluginInstalled": "Додаток успешно инсталиран",
+    "NotificationOptionPluginError": "Грешка на додаток",
+    "NotificationOptionNewLibraryContent": "Додадена нова содржина",
+    "NotificationOptionInstallationFailed": "Неуспешна Инсталација",
+    "NotificationOptionCameraImageUploaded": "Слика од камера поставена",
+    "NotificationOptionAudioPlaybackStopped": "Аудио стопирано",
+    "NotificationOptionAudioPlayback": "Аудио стартувано",
+    "NotificationOptionApplicationUpdateInstalled": "Надоградбата на Апликацијата е иснталирана",
+    "NotificationOptionApplicationUpdateAvailable": "Возможна надоградба на Апликацијата",
+    "NewVersionIsAvailable": "Нова верзија од Jellyfin е возможна за спуштање.",
+    "NameSeasonUnknown": "Непозната Сезона",
+    "NameSeasonNumber": "Сезона {0}",
+    "NameInstallFailed": "{0} неуспешна инсталација",
+    "MusicVideos": "Музички видеа",
+    "Music": "Музика",
+    "Movies": "Филмови",
+    "MixedContent": "Мешана содржина",
+    "MessageServerConfigurationUpdated": "Серверската конфигурација беше надградена",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Секцијата на конфигурација на сервер {0} беше надоградена",
+    "MessageApplicationUpdatedTo": "Jellyfin беше надограден до {0}",
+    "MessageApplicationUpdated": "Jellyfin Серверот беше надограден",
+    "Latest": "Последно",
+    "LabelRunningTimeValue": "Време на работа: {0}",
+    "LabelIpAddressValue": "ИП Адреса: {0}",
+    "ItemRemovedWithName": "{0} е избришано до Библиотеката",
+    "ItemAddedWithName": "{0} беше додадено во Библиотеката",
+    "Inherit": "Следно",
+    "HomeVideos": "Домашни Видеа",
+    "HeaderRecordingGroups": "Групи на снимање",
+    "HeaderNextUp": "Следно",
+    "HeaderLiveTV": "ТВ",
+    "HeaderFavoriteSongs": "Омилени Песни",
+    "HeaderFavoriteShows": "Омилени Серии",
+    "HeaderFavoriteEpisodes": "Омилени Епизоди",
+    "HeaderFavoriteArtists": "Омилени Изведувачи",
+    "HeaderFavoriteAlbums": "Омилени Албуми",
+    "HeaderContinueWatching": "Продолжи со гледање",
+    "HeaderCameraUploads": "Поставувања од камера",
+    "HeaderAlbumArtists": "Изведувачи од Албуми",
+    "Genres": "Жанрови",
+    "Folders": "Папки",
+    "Favorites": "Омилени",
+    "FailedLoginAttemptWithUserName": "Неуспешно поврзување од {0}",
+    "DeviceOnlineWithName": "{0} е приклучен",
+    "DeviceOfflineWithName": "{0} се исклучи",
+    "Collections": "Колекции",
+    "ChapterNameValue": "Дел {0}",
+    "Channels": "Канали",
+    "CameraImageUploadedFrom": "Нова слика од камера беше поставена од {0}",
+    "Books": "Книги",
+    "AuthenticationSucceededWithUserName": "{0} успешно поврзан",
+    "Artists": "Изведувач",
+    "Application": "Апликација",
+    "AppDeviceValues": "Аплиакција: {0}, Уред: {1}",
+    "Albums": "Албуми",
+    "VersionNumber": "Верзија {0}",
+    "ValueSpecialEpisodeName": "Специјално - {0}",
+    "ValueHasBeenAddedToLibrary": "{0} е додадено во твојата библиотека",
+    "UserStoppedPlayingItemWithValues": "{0} заврши со репродукција {1} во {2}",
+    "UserStartedPlayingItemWithValues": "{0} пушти {1} на {2}",
+    "UserPolicyUpdatedWithName": "Полисата на користење беше надоградена за {0}",
+    "UserPasswordChangedWithName": "Лозинката е сменета за корисникот {0}",
+    "UserOnlineFromDevice": "{0} е приклучен од {1}",
+    "UserOfflineFromDevice": "{0} е дисконектиран од {1}",
+    "UserLockedOutWithName": "Корисникот {0} е заклучен",
+    "UserDownloadingItemWithValues": "{0} се спушта {1}",
+    "UserDeletedWithName": "Корисникот {0} е избришан",
+    "UserCreatedWithName": "Корисникот {0} е креиран",
+    "User": "Корисник",
+    "TvShows": "ТВ Серии",
+    "System": "Систем",
+    "Sync": "Синхронизација",
+    "SubtitlesDownloadedForItem": "Спуштање превод за {0}",
+    "SubtitleDownloadFailureFromForItem": "Преводот неуспешно се спушти од {0} за {1}",
+    "StartupEmbyServerIsLoading": "Jellyfin Server се пушта. Ве молиме причекајте.",
+    "Songs": "Песни",
+    "Shows": "Серии",
+    "ServerNameNeedsToBeRestarted": "{0} треба да се рестартира",
+    "ScheduledTaskStartedWithName": "{0} започна"
+}

+ 5 - 5
Emby.Server.Implementations/Localization/Core/ms.json

@@ -1,9 +1,9 @@
 {
     "Albums": "Album-album",
-    "AppDeviceValues": "App: {0}, Device: {1}",
+    "AppDeviceValues": "Apl: {0}, Peranti: {1}",
     "Application": "Aplikasi",
-    "Artists": "Artis-artis",
-    "AuthenticationSucceededWithUserName": "{0} successfully authenticated",
+    "Artists": "Artis",
+    "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
     "Books": "Buku-buku",
     "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
     "Channels": "Saluran",
@@ -30,7 +30,7 @@
     "Inherit": "Inherit",
     "ItemAddedWithName": "{0} was added to the library",
     "ItemRemovedWithName": "{0} was removed from the library",
-    "LabelIpAddressValue": "Ip address: {0}",
+    "LabelIpAddressValue": "Alamat IP: {0}",
     "LabelRunningTimeValue": "Running time: {0}",
     "Latest": "Latest",
     "MessageApplicationUpdated": "Jellyfin Server has been updated",
@@ -50,7 +50,7 @@
     "NotificationOptionAudioPlayback": "Audio playback started",
     "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
     "NotificationOptionCameraImageUploaded": "Camera image uploaded",
-    "NotificationOptionInstallationFailed": "Installation failure",
+    "NotificationOptionInstallationFailed": "Pemasangan gagal",
     "NotificationOptionNewLibraryContent": "New content added",
     "NotificationOptionPluginError": "Plugin failure",
     "NotificationOptionPluginInstalled": "Plugin installed",

+ 2 - 2
Emby.Server.Implementations/Localization/Core/nl.json

@@ -3,13 +3,13 @@
     "AppDeviceValues": "App: {0}, Apparaat: {1}",
     "Application": "Applicatie",
     "Artists": "Artiesten",
-    "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
+    "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd",
     "Books": "Boeken",
     "CameraImageUploadedFrom": "Er is een nieuwe foto toegevoegd van {0}",
     "Channels": "Kanalen",
     "ChapterNameValue": "Hoofdstuk {0}",
     "Collections": "Verzamelingen",
-    "DeviceOfflineWithName": "{0} heeft de verbinding verbroken",
+    "DeviceOfflineWithName": "Verbinding met {0} is verbroken",
     "DeviceOnlineWithName": "{0} is verbonden",
     "FailedLoginAttemptWithUserName": "Mislukte aanmeld poging van {0}",
     "Favorites": "Favorieten",

+ 40 - 1
Emby.Server.Implementations/Localization/Core/nn.json

@@ -1 +1,40 @@
-{}
+{
+    "MessageServerConfigurationUpdated": "Tenar konfigurasjonen har blitt oppdatert",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Tenar konfigurasjon seksjon {0} har blitt oppdatert",
+    "MessageApplicationUpdatedTo": "Jellyfin Tenaren har blitt oppdatert til {0}",
+    "MessageApplicationUpdated": "Jellyfin Tenaren har blitt oppdatert",
+    "Latest": "Nyaste",
+    "LabelRunningTimeValue": "Speletid: {0}",
+    "LabelIpAddressValue": "IP adresse: {0}",
+    "ItemRemovedWithName": "{0} vart fjerna frå biblioteket",
+    "ItemAddedWithName": "{0} vart lagt til i biblioteket",
+    "Inherit": "Arv",
+    "HomeVideos": "Heime Videoar",
+    "HeaderRecordingGroups": "Innspelingsgrupper",
+    "HeaderNextUp": "Neste",
+    "HeaderLiveTV": "Direkte TV",
+    "HeaderFavoriteSongs": "Favoritt Songar",
+    "HeaderFavoriteShows": "Favoritt Seriar",
+    "HeaderFavoriteEpisodes": "Favoritt Episodar",
+    "HeaderFavoriteArtists": "Favoritt Artistar",
+    "HeaderFavoriteAlbums": "Favoritt Album",
+    "HeaderContinueWatching": "Fortsett å sjå",
+    "HeaderCameraUploads": "Kamera Opplastingar",
+    "HeaderAlbumArtists": "Album Artist",
+    "Genres": "Sjangrar",
+    "Folders": "Mapper",
+    "Favorites": "Favorittar",
+    "FailedLoginAttemptWithUserName": "Mislukka påloggingsforsøk frå {0}",
+    "DeviceOnlineWithName": "{0} er tilkopla",
+    "DeviceOfflineWithName": "{0} har kopla frå",
+    "Collections": "Samlingar",
+    "ChapterNameValue": "Kapittel {0}",
+    "Channels": "Kanalar",
+    "CameraImageUploadedFrom": "Eit nytt kamera bilete har blitt lasta opp frå {0}",
+    "Books": "Bøker",
+    "AuthenticationSucceededWithUserName": "{0} Har logga inn",
+    "Artists": "Artistar",
+    "Application": "Program",
+    "AppDeviceValues": "App: {0}, Einheit: {1}",
+    "Albums": "Album"
+}

+ 25 - 25
Emby.Server.Implementations/Localization/Core/pt.json

@@ -1,5 +1,5 @@
 {
-    "HeaderLiveTV": "TV ao Vivo",
+    "HeaderLiveTV": "TV em Directo",
     "Collections": "Colecções",
     "Books": "Livros",
     "Artists": "Artistas",
@@ -10,13 +10,13 @@
     "HeaderFavoriteAlbums": "Álbuns Favoritos",
     "HeaderFavoriteEpisodes": "Episódios Favoritos",
     "HeaderFavoriteShows": "Séries Favoritas",
-    "HeaderContinueWatching": "Continuar a Ver",
+    "HeaderContinueWatching": "Continuar a Assistir",
     "HeaderAlbumArtists": "Artistas do Álbum",
     "Genres": "Géneros",
-    "Folders": "Pastas",
+    "Folders": "Directórios",
     "Favorites": "Favoritos",
     "Channels": "Canais",
-    "UserDownloadingItemWithValues": "{0} está a transferir {1}",
+    "UserDownloadingItemWithValues": "{0} está a ser transferido {1}",
     "VersionNumber": "Versão {0}",
     "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia",
     "UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
@@ -24,12 +24,12 @@
     "UserPolicyUpdatedWithName": "A política do utilizador {0} foi alterada",
     "UserPasswordChangedWithName": "A palavra-passe do utilizador {0} foi alterada",
     "UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
-    "UserOfflineFromDevice": "{0} desligou-se a partir de {1}",
-    "UserLockedOutWithName": "Utilizador {0} bloqueado",
-    "UserDeletedWithName": "Utilizador {0} removido",
-    "UserCreatedWithName": "Utilizador {0} criado",
+    "UserOfflineFromDevice": "{0} desconectou-se a partir de {1}",
+    "UserLockedOutWithName": "O utilizador {0} foi bloqueado",
+    "UserDeletedWithName": "O utilizador {0} foi removido",
+    "UserCreatedWithName": "O utilizador {0} foi criado",
     "User": "Utilizador",
-    "TvShows": "Programas",
+    "TvShows": "Séries",
     "System": "Sistema",
     "SubtitlesDownloadedForItem": "Legendas transferidas para {0}",
     "SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}",
@@ -38,22 +38,22 @@
     "ScheduledTaskStartedWithName": "{0} iniciou",
     "ScheduledTaskFailedWithName": "{0} falhou",
     "ProviderValue": "Fornecedor: {0}",
-    "PluginUpdatedWithName": "{0} foi actualizado",
+    "PluginUpdatedWithName": "{0} foi atualizado",
     "PluginUninstalledWithName": "{0} foi desinstalado",
     "PluginInstalledWithName": "{0} foi instalado",
-    "Plugin": "Extensão",
+    "Plugin": "Plugin",
     "NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
     "NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
     "NotificationOptionUserLockedOut": "Utilizador bloqueado",
     "NotificationOptionTaskFailed": "Falha em tarefa agendada",
     "NotificationOptionServerRestartRequired": "É necessário reiniciar o servidor",
-    "NotificationOptionPluginUpdateInstalled": "Extensão actualizada",
-    "NotificationOptionPluginUninstalled": "Extensão desinstalada",
-    "NotificationOptionPluginInstalled": "Extensão instalada",
-    "NotificationOptionPluginError": "Falha na extensão",
+    "NotificationOptionPluginUpdateInstalled": "Plugin actualizado",
+    "NotificationOptionPluginUninstalled": "Plugin desinstalado",
+    "NotificationOptionPluginInstalled": "Plugin instalado",
+    "NotificationOptionPluginError": "Falha no plugin",
     "NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
     "NotificationOptionInstallationFailed": "Falha de instalação",
-    "NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
+    "NotificationOptionCameraImageUploaded": "Imagem de câmara enviada",
     "NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
     "NotificationOptionAudioPlayback": "Reprodução Iniciada",
     "NotificationOptionApplicationUpdateInstalled": "A actualização da aplicação foi instalada",
@@ -66,30 +66,30 @@
     "Music": "Música",
     "MixedContent": "Conteúdo Misto",
     "MessageServerConfigurationUpdated": "A configuração do servidor foi actualizada",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Configurações do servidor na secção {0} foram atualizadas",
-    "MessageApplicationUpdatedTo": "O servidor Jellyfin foi actualizado para a versão {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "As configurações do servidor na secção {0} foram atualizadas",
+    "MessageApplicationUpdatedTo": "O servidor Jellyfin foi atualizado para a versão {0}",
     "MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
     "Latest": "Mais Recente",
     "LabelRunningTimeValue": "Duração: {0}",
-    "LabelIpAddressValue": "Endereço IP: {0}",
+    "LabelIpAddressValue": "Endereço de IP: {0}",
     "ItemRemovedWithName": "{0} foi removido da biblioteca",
     "ItemAddedWithName": "{0} foi adicionado à biblioteca",
     "Inherit": "Herdar",
     "HomeVideos": "Vídeos Caseiros",
     "HeaderRecordingGroups": "Grupos de Gravação",
-    "ValueSpecialEpisodeName": "Especial - {0}",
+    "ValueSpecialEpisodeName": "Episódio Especial - {0}",
     "Sync": "Sincronização",
     "Songs": "Músicas",
     "Shows": "Séries",
     "Playlists": "Listas de Reprodução",
     "Photos": "Fotografias",
     "Movies": "Filmes",
-    "HeaderCameraUploads": "Envios a partir da câmara",
-    "FailedLoginAttemptWithUserName": "Tentativa de ligação  a partir de {0} falhou",
-    "DeviceOnlineWithName": "{0} ligou-se",
-    "DeviceOfflineWithName": "{0} desligou-se",
+    "HeaderCameraUploads": "Carregamentos a partir da câmara",
+    "FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}",
+    "DeviceOnlineWithName": "{0} está connectado",
+    "DeviceOfflineWithName": "{0} desconectou-se",
     "ChapterNameValue": "Capítulo {0}",
-    "CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
+    "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
     "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
     "Application": "Aplicação",
     "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}"

+ 16 - 16
Emby.Server.Implementations/Localization/Core/sv.json

@@ -1,7 +1,7 @@
 {
     "Albums": "Album",
-    "AppDeviceValues": "App: {0}, Enhet: {1}",
-    "Application": "App",
+    "AppDeviceValues": "Applikation: {0}, Enhet: {1}",
+    "Application": "Applikation",
     "Artists": "Artister",
     "AuthenticationSucceededWithUserName": "{0} har autentiserats",
     "Books": "Böcker",
@@ -16,15 +16,15 @@
     "Folders": "Mappar",
     "Genres": "Genrer",
     "HeaderAlbumArtists": "Albumartister",
-    "HeaderCameraUploads": "Kamera Uppladdningar",
-    "HeaderContinueWatching": "Fortsätt kolla",
+    "HeaderCameraUploads": "Kamerauppladdningar",
+    "HeaderContinueWatching": "Fortsätt kolla",
     "HeaderFavoriteAlbums": "Favoritalbum",
     "HeaderFavoriteArtists": "Favoritartister",
     "HeaderFavoriteEpisodes": "Favoritavsnitt",
     "HeaderFavoriteShows": "Favoritserier",
     "HeaderFavoriteSongs": "Favoritlåtar",
     "HeaderLiveTV": "Live-TV",
-    "HeaderNextUp": "Nästa på tur",
+    "HeaderNextUp": "Nästa",
     "HeaderRecordingGroups": "Inspelningsgrupper",
     "HomeVideos": "Hemvideor",
     "Inherit": "Ärv",
@@ -34,9 +34,9 @@
     "LabelRunningTimeValue": "Speltid: {0}",
     "Latest": "Senaste",
     "MessageApplicationUpdated": "Jellyfin Server har uppdaterats",
-    "MessageApplicationUpdatedTo": "Jellyfin Server har uppgraderats till {0}",
+    "MessageApplicationUpdatedTo": "Jellyfin Server har uppdaterats till {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Serverinställningarna {0} har uppdaterats",
-    "MessageServerConfigurationUpdated": "Server konfigurationen har uppdaterats",
+    "MessageServerConfigurationUpdated": "Serverkonfigurationen har uppdaterats",
     "MixedContent": "Blandat innehåll",
     "Movies": "Filmer",
     "Music": "Musik",
@@ -44,11 +44,11 @@
     "NameInstallFailed": "{0} installationen misslyckades",
     "NameSeasonNumber": "Säsong {0}",
     "NameSeasonUnknown": "Okänd säsong",
-    "NewVersionIsAvailable": "En ny version av Jellyfin Server är klar för nedladdning.",
+    "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig att hämta.",
     "NotificationOptionApplicationUpdateAvailable": "Ny programversion tillgänglig",
     "NotificationOptionApplicationUpdateInstalled": "Programuppdatering installerad",
     "NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats",
-    "NotificationOptionAudioPlaybackStopped": "Ljuduppspelning stoppad",
+    "NotificationOptionAudioPlaybackStopped": "Ljuduppspelning stoppades",
     "NotificationOptionCameraImageUploaded": "Kamerabild har laddats upp",
     "NotificationOptionInstallationFailed": "Fel vid installation",
     "NotificationOptionNewLibraryContent": "Nytt innehåll har lagts till",
@@ -60,7 +60,7 @@
     "NotificationOptionTaskFailed": "Schemalagd aktivitet har misslyckats",
     "NotificationOptionUserLockedOut": "Användare har låsts ut",
     "NotificationOptionVideoPlayback": "Videouppspelning har påbörjats",
-    "NotificationOptionVideoPlaybackStopped": "Videouppspelning stoppad",
+    "NotificationOptionVideoPlaybackStopped": "Videouppspelning stoppades",
     "Photos": "Bilder",
     "Playlists": "Spellistor",
     "Plugin": "Tillägg",
@@ -69,13 +69,13 @@
     "PluginUpdatedWithName": "{0} uppdaterades",
     "ProviderValue": "Källa: {0}",
     "ScheduledTaskFailedWithName": "{0} misslyckades",
-    "ScheduledTaskStartedWithName": "{0} startad",
+    "ScheduledTaskStartedWithName": "{0} startades",
     "ServerNameNeedsToBeRestarted": "{0} behöver startas om",
     "Shows": "Serier",
     "Songs": "Låtar",
-    "StartupEmbyServerIsLoading": "Jellyfin server arbetar. Pröva igen inom kort.",
+    "StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.",
     "SubtitleDownloadFailureForItem": "Nerladdning av undertexter för {0} misslyckades",
-    "SubtitleDownloadFailureFromForItem": "Undertexter misslyckades att ladda ner  {0} för {1}",
+    "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} för {1}",
     "SubtitlesDownloadedForItem": "Undertexter har laddats ner till {0}",
     "Sync": "Synk",
     "System": "System",
@@ -89,9 +89,9 @@
     "UserOnlineFromDevice": "{0} är uppkopplad från {1}",
     "UserPasswordChangedWithName": "Lösenordet för {0} har ändrats",
     "UserPolicyUpdatedWithName": "Användarpolicyn har uppdaterats för {0}",
-    "UserStartedPlayingItemWithValues": "{0} har börjat spela upp {1}",
-    "UserStoppedPlayingItemWithValues": "{0} har avslutat uppspelningen av {1}",
-    "ValueHasBeenAddedToLibrary": "{0} har blivit tillagd till ditt mediabibliotek",
+    "UserStartedPlayingItemWithValues": "{0} spelar upp {1} på {2}",
+    "UserStoppedPlayingItemWithValues": "{0} har avslutat uppspelningen av {1} på {2}",
+    "ValueHasBeenAddedToLibrary": "{0} har lagts till i ditt mediebibliotek",
     "ValueSpecialEpisodeName": "Specialavsnitt - {0}",
     "VersionNumber": "Version {0}"
 }

+ 2 - 2
Emby.Server.Implementations/Localization/Core/zh-CN.json

@@ -17,14 +17,14 @@
     "Genres": "风格",
     "HeaderAlbumArtists": "专辑作家",
     "HeaderCameraUploads": "相机上传",
-    "HeaderContinueWatching": "继续观",
+    "HeaderContinueWatching": "继续观",
     "HeaderFavoriteAlbums": "收藏的专辑",
     "HeaderFavoriteArtists": "最爱的艺术家",
     "HeaderFavoriteEpisodes": "最爱的剧集",
     "HeaderFavoriteShows": "最爱的节目",
     "HeaderFavoriteSongs": "最爱的歌曲",
     "HeaderLiveTV": "电视直播",
-    "HeaderNextUp": "下一步",
+    "HeaderNextUp": "接下来",
     "HeaderRecordingGroups": "录制组",
     "HomeVideos": "家庭视频",
     "Inherit": "继承",

+ 2 - 2
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -33,14 +33,14 @@ namespace Emby.Server.Implementations.Playlists
             ILibraryManager libraryManager,
             IFileSystem fileSystem,
             ILibraryMonitor iLibraryMonitor,
-            ILoggerFactory loggerFactory,
+            ILogger<PlaylistManager> logger,
             IUserManager userManager,
             IProviderManager providerManager)
         {
             _libraryManager = libraryManager;
             _fileSystem = fileSystem;
             _iLibraryMonitor = iLibraryMonitor;
-            _logger = loggerFactory.CreateLogger(nameof(PlaylistManager));
+            _logger = logger;
             _userManager = userManager;
             _providerManager = providerManager;
         }

+ 4 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -29,7 +29,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// <summary>
         /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
         /// </summary>
-        public DeleteCacheFileTask(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem)
+        public DeleteCacheFileTask(
+            IApplicationPaths appPaths,
+            ILogger<DeleteCacheFileTask> logger,
+            IFileSystem fileSystem)
         {
             ApplicationPaths = appPaths;
             _logger = logger;

+ 4 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs

@@ -23,7 +23,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// <summary>
         /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask" /> class.
         /// </summary>
-        public DeleteTranscodeFileTask(ILogger logger, IFileSystem fileSystem, IConfigurationManager configurationManager)
+        public DeleteTranscodeFileTask(
+            ILogger<DeleteTranscodeFileTask> logger,
+            IFileSystem fileSystem,
+            IConfigurationManager configurationManager)
         {
             _logger = logger;
             _fileSystem = fileSystem;

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs

@@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
         private readonly IInstallationManager _installationManager;
 
-        public PluginUpdateTask(ILogger logger, IInstallationManager installationManager)
+        public PluginUpdateTask(ILogger<PluginUpdateTask> logger, IInstallationManager installationManager)
         {
             _logger = logger;
             _installationManager = installationManager;

+ 2 - 3
Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs

@@ -21,15 +21,14 @@ namespace Emby.Server.Implementations.SocketSharp
         private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
         private CancellationToken _disposeCancellationToken;
 
-        public WebSocketSharpListener(
-            ILogger logger)
+        public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger)
         {
             _logger = logger;
-
             _disposeCancellationToken = _disposeCancellationTokenSource.Token;
         }
 
         public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
+
         public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
 
         public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }

+ 0 - 99
Emby.Server.Implementations/Sorting/AlphanumComparator.cs

@@ -1,99 +0,0 @@
-using System.Collections.Generic;
-using System.Text;
-using MediaBrowser.Controller.Sorting;
-
-namespace Emby.Server.Implementations.Sorting
-{
-    public class AlphanumComparator : IComparer<string>
-    {
-        public static int CompareValues(string s1, string s2)
-        {
-            if (s1 == null || s2 == null)
-            {
-                return 0;
-            }
-
-            int thisMarker = 0, thisNumericChunk = 0;
-            int thatMarker = 0, thatNumericChunk = 0;
-
-            while ((thisMarker < s1.Length) || (thatMarker < s2.Length))
-            {
-                if (thisMarker >= s1.Length)
-                {
-                    return -1;
-                }
-                else if (thatMarker >= s2.Length)
-                {
-                    return 1;
-                }
-                char thisCh = s1[thisMarker];
-                char thatCh = s2[thatMarker];
-
-                var thisChunk = new StringBuilder();
-                var thatChunk = new StringBuilder();
-
-                while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0])))
-                {
-                    thisChunk.Append(thisCh);
-                    thisMarker++;
-
-                    if (thisMarker < s1.Length)
-                    {
-                        thisCh = s1[thisMarker];
-                    }
-                }
-
-                while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || SortHelper.InChunk(thatCh, thatChunk[0])))
-                {
-                    thatChunk.Append(thatCh);
-                    thatMarker++;
-
-                    if (thatMarker < s2.Length)
-                    {
-                        thatCh = s2[thatMarker];
-                    }
-                }
-
-                int result = 0;
-                // If both chunks contain numeric characters, sort them numerically
-                if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0]))
-                {
-                    if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk))
-                    {
-                        return 0;
-                    }
-                    if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk))
-                    {
-                        return 0;
-                    }
-
-                    if (thisNumericChunk < thatNumericChunk)
-                    {
-                        result = -1;
-                    }
-
-                    if (thisNumericChunk > thatNumericChunk)
-                    {
-                        result = 1;
-                    }
-                }
-                else
-                {
-                    result = thisChunk.ToString().CompareTo(thatChunk.ToString());
-                }
-
-                if (result != 0)
-                {
-                    return result;
-                }
-            }
-
-            return 0;
-        }
-
-        public int Compare(string x, string y)
-        {
-            return CompareValues(x, y);
-        }
-    }
-}

+ 2 - 5
Jellyfin.Server/CoreAppHost.cs

@@ -23,23 +23,20 @@ namespace Jellyfin.Server
         /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="imageEncoder">The <see cref="IImageEncoder" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
-        /// <param name="configuration">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
         public CoreAppHost(
             ServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             StartupOptions options,
             IFileSystem fileSystem,
             IImageEncoder imageEncoder,
-            INetworkManager networkManager,
-            IConfiguration configuration)
+            INetworkManager networkManager)
             : base(
                 applicationPaths,
                 loggerFactory,
                 options,
                 fileSystem,
                 imageEncoder,
-                networkManager,
-                configuration)
+                networkManager)
         {
         }
 

+ 28 - 0
Jellyfin.Server/Migrations/IMigrationRoutine.cs

@@ -0,0 +1,28 @@
+using System;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations
+{
+    /// <summary>
+    /// Interface that describes a migration routine.
+    /// </summary>
+    internal interface IMigrationRoutine
+    {
+        /// <summary>
+        /// Gets the unique id for this migration. This should never be modified after the migration has been created.
+        /// </summary>
+        public Guid Id { get; }
+
+        /// <summary>
+        /// Gets the display name of the migration.
+        /// </summary>
+        public string Name { get; }
+
+        /// <summary>
+        /// Execute the migration routine.
+        /// </summary>
+        /// <param name="host">Host that hosts current version.</param>
+        /// <param name="logger">Host logger.</param>
+        public void Perform(CoreAppHost host, ILogger logger);
+    }
+}

+ 24 - 0
Jellyfin.Server/Migrations/MigrationOptions.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Server.Migrations
+{
+    /// <summary>
+    /// Configuration part that holds all migrations that were applied.
+    /// </summary>
+    public class MigrationOptions
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MigrationOptions"/> class.
+        /// </summary>
+        public MigrationOptions()
+        {
+            Applied = new List<(Guid Id, string Name)>();
+        }
+
+        /// <summary>
+        /// Gets the list of applied migration routine names.
+        /// </summary>
+        public List<(Guid Id, string Name)> Applied { get; }
+    }
+}

+ 73 - 0
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -0,0 +1,73 @@
+using System;
+using System.Linq;
+using MediaBrowser.Common.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations
+{
+    /// <summary>
+    /// The class that knows which migrations to apply and how to apply them.
+    /// </summary>
+    public sealed class MigrationRunner
+    {
+        /// <summary>
+        /// The list of known migrations, in order of applicability.
+        /// </summary>
+        internal static readonly IMigrationRoutine[] Migrations =
+        {
+            new Routines.DisableTranscodingThrottling(),
+            new Routines.CreateUserLoggingConfigFile()
+        };
+
+        /// <summary>
+        /// Run all needed migrations.
+        /// </summary>
+        /// <param name="host">CoreAppHost that hosts current version.</param>
+        /// <param name="loggerFactory">Factory for making the logger.</param>
+        public static void Run(CoreAppHost host, ILoggerFactory loggerFactory)
+        {
+            var logger = loggerFactory.CreateLogger<MigrationRunner>();
+            var migrationOptions = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
+
+            if (!host.ServerConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0)
+            {
+                // If startup wizard is not finished, this is a fresh install.
+                // Don't run any migrations, just mark all of them as applied.
+                logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
+                migrationOptions.Applied.AddRange(Migrations.Select(m => (m.Id, m.Name)));
+                host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+                return;
+            }
+
+            var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
+
+            for (var i = 0; i < Migrations.Length; i++)
+            {
+                var migrationRoutine = Migrations[i];
+                if (appliedMigrationIds.Contains(migrationRoutine.Id))
+                {
+                    logger.LogDebug("Skipping migration '{Name}' since it is already applied", migrationRoutine.Name);
+                    continue;
+                }
+
+                logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name);
+
+                try
+                {
+                    migrationRoutine.Perform(host, logger);
+                }
+                catch (Exception ex)
+                {
+                    logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name);
+                    throw;
+                }
+
+                // Mark the migration as completed
+                logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name);
+                migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name));
+                host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+                logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name);
+            }
+        }
+    }
+}

+ 20 - 0
Jellyfin.Server/Migrations/MigrationsFactory.cs

@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+
+namespace Jellyfin.Server.Migrations
+{
+    /// <summary>
+    /// A factory that can find a persistent file of the migration configuration, which lists all applied migrations.
+    /// </summary>
+    public class MigrationsFactory : IConfigurationFactory
+    {
+        /// <inheritdoc/>
+        public IEnumerable<ConfigurationStore> GetConfigurations()
+        {
+            return new[]
+            {
+                new MigrationsListStore()
+            };
+        }
+    }
+}

+ 24 - 0
Jellyfin.Server/Migrations/MigrationsListStore.cs

@@ -0,0 +1,24 @@
+using MediaBrowser.Common.Configuration;
+
+namespace Jellyfin.Server.Migrations
+{
+    /// <summary>
+    /// A configuration that lists all the migration routines that were applied.
+    /// </summary>
+    public class MigrationsListStore : ConfigurationStore
+    {
+        /// <summary>
+        /// The name of the configuration in the storage.
+        /// </summary>
+        public static readonly string StoreKey = "migrations";
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MigrationsListStore"/> class.
+        /// </summary>
+        public MigrationsListStore()
+        {
+            ConfigurationType = typeof(MigrationOptions);
+            Key = StoreKey;
+        }
+    }
+}

+ 73 - 0
Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs

@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.Configuration;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+    /// <summary>
+    /// Migration to initialize the user logging configuration file "logging.user.json".
+    /// If the deprecated logging.json file exists and has a custom config, it will be used as logging.user.json,
+    /// otherwise a blank file will be created.
+    /// </summary>
+    internal class CreateUserLoggingConfigFile : IMigrationRoutine
+    {
+        /// <summary>
+        /// File history for logging.json as existed during this migration creation. The contents for each has been minified.
+        /// </summary>
+        private readonly List<string> _defaultConfigHistory = new List<string>
+        {
+            // 9a6c27947353585391e211aa88b925f81e8cd7b9
+            @"{""Serilog"":{""MinimumLevel"":{""Default"":""Information"",""Override"":{""Microsoft"":""Warning"",""System"":""Warning""}},""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}",
+            // 71bdcd730705a714ee208eaad7290b7c68df3885
+            @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}",
+            // a44936f97f8afc2817d3491615a7cfe1e31c251c
+            @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}",
+            // 7af3754a11ad5a4284f107997fb5419a010ce6f3
+            @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}]}}",
+            // 60691349a11f541958e0b2247c9abc13cb40c9fb
+            @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}""}}]}}]}}",
+            // 65fe243afbcc4b596cf8726708c1965cd34b5f68
+            @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] {ThreadId} {SourceContext}: {Message:lj} {NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {ThreadId} {SourceContext}:{Message} {NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}",
+            // 96c9af590494aa8137d5a061aaf1e68feee60b67
+            @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}",
+        };
+
+        /// <inheritdoc/>
+        public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}");
+
+        /// <inheritdoc/>
+        public string Name => "CreateLoggingConfigHeirarchy";
+
+        /// <inheritdoc/>
+        public void Perform(CoreAppHost host, ILogger logger)
+        {
+            var logDirectory = host.Resolve<IApplicationPaths>().ConfigurationDirectoryPath;
+            var existingConfigPath = Path.Combine(logDirectory, "logging.json");
+
+            // If the existing logging.json config file is unmodified, then 'reset' it by moving it to 'logging.old.json'
+            // NOTE: This config file has 'reloadOnChange: true', so this change will take effect immediately even though it has already been loaded
+            if (File.Exists(existingConfigPath) && ExistingConfigUnmodified(existingConfigPath))
+            {
+                File.Move(existingConfigPath, Path.Combine(logDirectory, "logging.old.json"));
+            }
+        }
+
+        /// <summary>
+        /// Check if the existing logging.json file has not been modified by the user by comparing it to all the
+        /// versions in our git history. Until now, the file has never been migrated after first creation so users
+        /// could have any version from the git history.
+        /// </summary>
+        /// <exception cref="IOException"><paramref name="oldConfigPath"/> does not exist or could not be read.</exception>
+        private bool ExistingConfigUnmodified(string oldConfigPath)
+        {
+            var existingConfigJson = JToken.Parse(File.ReadAllText(oldConfigPath));
+            return _defaultConfigHistory
+                .Select(historicalConfigText => JToken.Parse(historicalConfigText))
+                .Any(historicalConfigJson => JToken.DeepEquals(existingConfigJson, historicalConfigJson));
+        }
+    }
+}

+ 35 - 0
Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs

@@ -0,0 +1,35 @@
+using System;
+using System.IO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+    /// <summary>
+    /// Disable transcode throttling for all installations since it is currently broken for certain video formats.
+    /// </summary>
+    internal class DisableTranscodingThrottling : IMigrationRoutine
+    {
+        /// <inheritdoc/>
+        public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}");
+
+        /// <inheritdoc/>
+        public string Name => "DisableTranscodingThrottling";
+
+        /// <inheritdoc/>
+        public void Perform(CoreAppHost host, ILogger logger)
+        {
+            // Set EnableThrottling to false since it wasn't used before and may introduce issues
+            var encoding = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<EncodingOptions>("encoding");
+            if (encoding.EnableThrottling)
+            {
+                logger.LogInformation("Disabling transcoding throttling during migration");
+                encoding.EnableThrottling = false;
+
+                host.ServerConfigurationManager.SaveConfiguration("encoding", encoding);
+            }
+        }
+    }
+}

+ 61 - 32
Jellyfin.Server/Program.cs

@@ -26,6 +26,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 using Serilog;
+using Serilog.Events;
 using Serilog.Extensions.Logging;
 using SQLitePCL;
 using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -37,6 +38,16 @@ namespace Jellyfin.Server
     /// </summary>
     public static class Program
     {
+        /// <summary>
+        /// The name of logging configuration file containing application defaults.
+        /// </summary>
+        public static readonly string LoggingConfigFileDefault = "logging.default.json";
+
+        /// <summary>
+        /// The name of the logging configuration file containing the system-specific override settings.
+        /// </summary>
+        public static readonly string LoggingConfigFileSystem = "logging.json";
+
         private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
         private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
         private static ILogger _logger = NullLogger.Instance;
@@ -101,10 +112,12 @@ namespace Jellyfin.Server
             // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
             Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
 
-            IConfiguration appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false);
-
-            CreateLogger(appConfig, appPaths);
+            // Create an instance of the application configuration to use for application startup
+            await InitLoggingConfigFile(appPaths).ConfigureAwait(false);
+            IConfiguration startupConfig = CreateAppConfiguration(appPaths);
 
+            // Initialize logging framework
+            InitializeLoggingFramework(startupConfig, appPaths);
             _logger = _loggerFactory.CreateLogger("Main");
 
             // Log uncaught exceptions to the logging instead of std error
@@ -169,22 +182,22 @@ namespace Jellyfin.Server
                 options,
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
                 GetImageEncoder(appPaths),
-                new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
-                appConfig);
+                new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()));
             try
             {
                 ServiceCollection serviceCollection = new ServiceCollection();
-                await appHost.InitAsync(serviceCollection).ConfigureAwait(false);
+                await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false);
 
-                var host = CreateWebHostBuilder(appHost, serviceCollection).Build();
+                var webHost = CreateWebHostBuilder(appHost, serviceCollection, appPaths).Build();
 
                 // A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection.
-                appHost.ServiceProvider = host.Services;
+                appHost.ServiceProvider = webHost.Services;
                 appHost.FindParts();
+                Migrations.MigrationRunner.Run(appHost, _loggerFactory);
 
                 try
                 {
-                    await host.StartAsync().ConfigureAwait(false);
+                    await webHost.StartAsync().ConfigureAwait(false);
                 }
                 catch
                 {
@@ -220,7 +233,7 @@ namespace Jellyfin.Server
             }
         }
 
-        private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection)
+        private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection, IApplicationPaths appPaths)
         {
             return new WebHostBuilder()
                 .UseKestrel(options =>
@@ -260,6 +273,8 @@ namespace Jellyfin.Server
                         }
                     }
                 })
+                .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths))
+                .UseSerilog()
                 .UseContentRoot(appHost.ContentRoot)
                 .ConfigureServices(services =>
                 {
@@ -432,37 +447,51 @@ namespace Jellyfin.Server
             return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir, webDir);
         }
 
-        private static async Task<IConfiguration> CreateConfiguration(IApplicationPaths appPaths)
+        /// <summary>
+        /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
+        /// already.
+        /// </summary>
+        private static async Task InitLoggingConfigFile(IApplicationPaths appPaths)
         {
-            const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
-            string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json");
-
-            if (!File.Exists(configPath))
+            // Do nothing if the config file already exists
+            string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault);
+            if (File.Exists(configPath))
             {
-                // For some reason the csproj name is used instead of the assembly name
-                await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath);
-                if (resource == null)
-                {
-                    throw new InvalidOperationException(
-                        string.Format(
-                            CultureInfo.InvariantCulture,
-                            "Invalid resource path: '{0}'",
-                            ResourcePath));
-                }
-
-                await using Stream dst = File.Open(configPath, FileMode.CreateNew);
-                await resource.CopyToAsync(dst).ConfigureAwait(false);
+                return;
             }
 
+            // Get a stream of the resource contents
+            // NOTE: The .csproj name is used instead of the assembly name in the resource path
+            const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
+            await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
+                ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
+
+            // Copy the resource contents to the expected file path for the config file
+            await using Stream dst = File.Open(configPath, FileMode.CreateNew);
+            await resource.CopyToAsync(dst).ConfigureAwait(false);
+        }
+
+        private static IConfiguration CreateAppConfiguration(IApplicationPaths appPaths)
+        {
             return new ConfigurationBuilder()
+                .ConfigureAppConfiguration(appPaths)
+                .Build();
+        }
+
+        private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths)
+        {
+            return config
                 .SetBasePath(appPaths.ConfigurationDirectoryPath)
                 .AddInMemoryCollection(ConfigurationOptions.Configuration)
-                .AddJsonFile("logging.json", false, true)
-                .AddEnvironmentVariables("JELLYFIN_")
-                .Build();
+                .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true)
+                .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true)
+                .AddEnvironmentVariables("JELLYFIN_");
         }
 
-        private static void CreateLogger(IConfiguration configuration, IApplicationPaths appPaths)
+        /// <summary>
+        /// Initialize Serilog using configuration and fall back to defaults on failure.
+        /// </summary>
+        private static void InitializeLoggingFramework(IConfiguration configuration, IApplicationPaths appPaths)
         {
             try
             {

+ 7 - 1
Jellyfin.Server/Resources/Configuration/logging.json

@@ -1,6 +1,12 @@
 {
     "Serilog": {
-        "MinimumLevel": "Information",
+        "MinimumLevel": {
+            "Default": "Information",
+            "Override": {
+                "Microsoft": "Warning",
+                "System": "Warning"
+            }
+        },
         "WriteTo": [
             {
                 "Name": "Console",

+ 1 - 1
MediaBrowser.Api/ApiEntryPoint.cs

@@ -61,7 +61,7 @@ namespace MediaBrowser.Api
         /// <param name="fileSystem">The file system.</param>
         /// <param name="mediaSourceManager">The media source manager.</param>
         public ApiEntryPoint(
-            ILogger logger,
+            ILogger<ApiEntryPoint> logger,
             ISessionManager sessionManager,
             IServerConfigurationManager config,
             IFileSystem fileSystem,

+ 1 - 1
MediaBrowser.Api/Library/LibraryService.cs

@@ -815,7 +815,7 @@ namespace MediaBrowser.Api.Library
             if (!string.IsNullOrWhiteSpace(filename))
             {
                 // Kestrel doesn't support non-ASCII characters in headers
-                if (Regex.IsMatch(filename, "[^[:ascii:]]"))
+                if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]"))
                 {
                     // 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);

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