aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md147
-rw-r--r--Dockerfile20
-rw-r--r--Dockerfile.arm31
-rw-r--r--Dockerfile.arm6421
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs5
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs63
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json40
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json4
-rw-r--r--Jellyfin.Server/Migrations/IMigrationRoutine.cs28
-rw-r--r--Jellyfin.Server/Migrations/MigrationOptions.cs24
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs73
-rw-r--r--Jellyfin.Server/Migrations/MigrationsFactory.cs20
-rw-r--r--Jellyfin.Server/Migrations/MigrationsListStore.cs24
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs73
-rw-r--r--Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs35
-rw-r--r--Jellyfin.Server/Program.cs18
-rw-r--r--Jellyfin.Server/Resources/Configuration/logging.json8
-rw-r--r--MediaBrowser.Api/Library/LibraryService.cs2
-rw-r--r--MediaBrowser.Api/Playback/MediaInfoService.cs3
-rw-r--r--MediaBrowser.Common/Extensions/ShuffleExtensions.cs13
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs45
-rw-r--r--MediaBrowser.Controller/Sorting/AlphanumComparator.cs130
-rw-r--r--MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs7
-rw-r--r--MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs21
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs2
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs2
-rw-r--r--MediaBrowser.sln7
-rw-r--r--deployment/fedora-package-x64/Dockerfile2
-rw-r--r--deployment/windows/jellyfin.nsi2
-rw-r--r--tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs44
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj21
35 files changed, 819 insertions, 186 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index e14636a57..4f3624965 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -1,41 +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)
- - [artiume](https://github.com/Artiume)
-
+ - [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
diff --git a/Dockerfile b/Dockerfile
index 1029a4cee..73ab3d790 100644
--- a/Dockerfile
+++ b/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 \
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 5847de918..4c7aa6aa7 100644
--- a/Dockerfile.arm
+++ b/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 -s 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"]
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index a9f6c50d9..9dc6fa7ed 100644
--- a/Dockerfile.arm64
+++ b/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
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 8ea188724..679ef4851 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -672,9 +672,8 @@ namespace Emby.Server.Implementations
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>();
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 5e672f221..fa6d57466 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -1,65 +1,68 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1600
-
using System;
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
+ };
}
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 019736c47..69678c268 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/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",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index d02f841fd..a38103d25 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index f3a7794da..5618719dd 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -2,28 +2,28 @@
"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": "סרטונים בייתים",
@@ -40,8 +40,8 @@
"MixedContent": "תוכן מעורב",
"Movies": "סרטים",
"Music": "מוזיקה",
- "MusicVideos": "Music videos",
- "NameInstallFailed": "{0} installation failed",
+ "MusicVideos": "קליפים",
+ "NameInstallFailed": "התקנת {0} נכשלה",
"NameSeasonNumber": "עונה {0}",
"NameSeasonUnknown": "עונה לא ידועה",
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
@@ -89,8 +89,8 @@
"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}"
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 4423b7f98..e22f95ab4 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/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",
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index db4cfde95..b2934545d 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/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,7 +16,7 @@
"Folders": "Mappar",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumartister",
- "HeaderCameraUploads": "Kamera Uppladdningar",
+ "HeaderCameraUploads": "Kamerauppladdningar",
"HeaderContinueWatching": "Fortsätt kolla",
"HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritartister",
@@ -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}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index dd6168614..68134a151 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/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": "继承",
diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs
new file mode 100644
index 000000000..eab995d67
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationOptions.cs b/Jellyfin.Server/Migrations/MigrationOptions.cs
new file mode 100644
index 000000000..816dd9ee7
--- /dev/null
+++ b/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; }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
new file mode 100644
index 000000000..b5ea04dca
--- /dev/null
+++ b/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);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationsFactory.cs b/Jellyfin.Server/Migrations/MigrationsFactory.cs
new file mode 100644
index 000000000..23c1b1ee6
--- /dev/null
+++ b/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()
+ };
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/MigrationsListStore.cs b/Jellyfin.Server/Migrations/MigrationsListStore.cs
new file mode 100644
index 000000000..7a1ca6671
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
new file mode 100644
index 000000000..3bc32c047
--- /dev/null
+++ b/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));
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
new file mode 100644
index 000000000..673f0e415
--- /dev/null
+++ b/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);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 1dd598236..7c3d0f277 100644
--- a/Jellyfin.Server/Program.cs
+++ b/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;
@@ -181,6 +192,7 @@ namespace Jellyfin.Server
// A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection.
appHost.ServiceProvider = host.Services;
appHost.FindParts();
+ Migrations.MigrationRunner.Run(appHost, _loggerFactory);
try
{
@@ -260,6 +272,7 @@ namespace Jellyfin.Server
}
}
})
+ .UseSerilog()
.UseContentRoot(appHost.ContentRoot)
.ConfigureServices(services =>
{
@@ -435,7 +448,7 @@ namespace Jellyfin.Server
private static async Task<IConfiguration> CreateConfiguration(IApplicationPaths appPaths)
{
const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
- string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json");
+ string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault);
if (!File.Exists(configPath))
{
@@ -457,7 +470,8 @@ namespace Jellyfin.Server
return new ConfigurationBuilder()
.SetBasePath(appPaths.ConfigurationDirectoryPath)
.AddInMemoryCollection(ConfigurationOptions.Configuration)
- .AddJsonFile("logging.json", false, true)
+ .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true)
+ .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true)
.AddEnvironmentVariables("JELLYFIN_")
.Build();
}
diff --git a/Jellyfin.Server/Resources/Configuration/logging.json b/Jellyfin.Server/Resources/Configuration/logging.json
index acbca8b85..f64a85219 100644
--- a/Jellyfin.Server/Resources/Configuration/logging.json
+++ b/Jellyfin.Server/Resources/Configuration/logging.json
@@ -1,6 +1,12 @@
{
"Serilog": {
- "MinimumLevel": "Information",
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "System": "Warning"
+ }
+ },
"WriteTo": [
{
"Name": "Console",
diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs
index 3d1e4a363..15284958d 100644
--- a/MediaBrowser.Api/Library/LibraryService.cs
+++ b/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);
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
index a44e1720f..08a7e534f 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/MediaBrowser.Api/Playback/MediaInfoService.cs
@@ -573,7 +573,8 @@ namespace MediaBrowser.Api.Playback
{
attachment.DeliveryUrl = string.Format(
CultureInfo.InvariantCulture,
- "/Videos/{0}/{1}/Attachments/{2}",
+ "{0}/Videos/{1}/{2}/Attachments/{3}",
+ ServerConfigurationManager.Configuration.BaseUrl,
item.Id,
mediaSource.Id,
attachment.Index);
diff --git a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs
index 5889d09c4..0432f36b5 100644
--- a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs
+++ b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs
@@ -17,11 +17,22 @@ namespace MediaBrowser.Common.Extensions
/// <typeparam name="T">The type.</typeparam>
public static void Shuffle<T>(this IList<T> list)
{
+ list.Shuffle(_rng);
+ }
+
+ /// <summary>
+ /// Shuffles the items in a list.
+ /// </summary>
+ /// <param name="list">The list that should get shuffled.</param>
+ /// <param name="rng">The random number generator to use.</param>
+ /// <typeparam name="T">The type.</typeparam>
+ public static void Shuffle<T>(this IList<T> list, Random rng)
+ {
int n = list.Count;
while (n > 1)
{
n--;
- int k = _rng.Next(n + 1);
+ int k = rng.Next(n + 1);
T value = list[k];
list[k] = list[n];
list[n] = value;
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index a892be7a9..c72bd487e 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -807,11 +807,45 @@ namespace MediaBrowser.Controller.Entities
return false;
}
+ private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
+ {
+ var ids = query.ItemIds;
+ int size = items.Count;
+
+ // ids can potentially contain non-unique guids, but query result cannot,
+ // so we include only first occurrence of each guid
+ var positions = new Dictionary<Guid, int>(size);
+ int index = 0;
+ for (int i = 0; i < ids.Length; i++)
+ {
+ if (positions.TryAdd(ids[i], index))
+ {
+ index++;
+ }
+ }
+
+ var newItems = new BaseItem[size];
+ for (int i = 0; i < size; i++)
+ {
+ var item = items[i];
+ newItems[positions[item.Id]] = item;
+ }
+
+ return newItems;
+ }
+
public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
{
if (query.ItemIds.Length > 0)
{
- return LibraryManager.GetItemsResult(query);
+ var result = LibraryManager.GetItemsResult(query);
+
+ if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
+ {
+ result.Items = SortItemsByRequest(query, result.Items);
+ }
+
+ return result;
}
return GetItemsInternal(query);
@@ -823,7 +857,14 @@ namespace MediaBrowser.Controller.Entities
if (query.ItemIds.Length > 0)
{
- return LibraryManager.GetItemList(query);
+ var result = LibraryManager.GetItemList(query);
+
+ if (query.OrderBy.Count == 0 && query.ItemIds.Length > 1)
+ {
+ return SortItemsByRequest(query, result);
+ }
+
+ return result.ToArray();
}
return GetItemsInternal(query).Items;
diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
index 65dc120ca..de7f72d1c 100644
--- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
+++ b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
@@ -1,94 +1,132 @@
+#nullable enable
+
+using System;
using System.Collections.Generic;
-using System.Text;
-using MediaBrowser.Controller.Sorting;
namespace MediaBrowser.Controller.Sorting
{
- public class AlphanumComparator : IComparer<string>
+ public class AlphanumComparator : IComparer<string?>
{
- public static int CompareValues(string s1, string s2)
+ public static int CompareValues(string? s1, string? s2)
{
- if (s1 == null || s2 == null)
+ if (s1 == null && s2 == null)
{
return 0;
}
+ else if (s1 == null)
+ {
+ return -1;
+ }
+ else if (s2 == null)
+ {
+ return 1;
+ }
- int thisMarker = 0, thisNumericChunk = 0;
- int thatMarker = 0, thatNumericChunk = 0;
+ int len1 = s1.Length;
+ int len2 = s2.Length;
- while ((thisMarker < s1.Length) || (thatMarker < s2.Length))
+ // Early return for empty strings
+ if (len1 == 0 && len2 == 0)
{
- if (thisMarker >= s1.Length)
+ return 0;
+ }
+ else if (len1 == 0)
+ {
+ return -1;
+ }
+ else if (len2 == 0)
+ {
+ return 1;
+ }
+
+ int pos1 = 0;
+ int pos2 = 0;
+
+ do
+ {
+ int start1 = pos1;
+ int start2 = pos2;
+
+ bool isNum1 = char.IsDigit(s1[pos1++]);
+ bool isNum2 = char.IsDigit(s2[pos2++]);
+
+ while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1)
{
- return -1;
+ pos1++;
}
- else if (thatMarker >= s2.Length)
+
+ while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2)
{
- return 1;
+ pos2++;
}
- char thisCh = s1[thisMarker];
- char thatCh = s2[thatMarker];
- var thisChunk = new StringBuilder();
- var thatChunk = new StringBuilder();
- bool thisNumeric = char.IsDigit(thisCh), thatNumeric = char.IsDigit(thatCh);
+ var span1 = s1.AsSpan(start1, pos1 - start1);
+ var span2 = s2.AsSpan(start2, pos2 - start2);
- while (thisMarker < s1.Length && char.IsDigit(thisCh) == thisNumeric)
+ if (isNum1 && isNum2)
{
- thisChunk.Append(thisCh);
- thisMarker++;
-
- if (thisMarker < s1.Length)
+ // Trim leading zeros so we can compare the length
+ // of the strings to find the largest number
+ span1 = span1.TrimStart('0');
+ span2 = span2.TrimStart('0');
+ var span1Len = span1.Length;
+ var span2Len = span2.Length;
+ if (span1Len < span2Len)
{
- thisCh = s1[thisMarker];
+ return -1;
}
- }
-
- while (thatMarker < s2.Length && char.IsDigit(thatCh) == thatNumeric)
- {
- thatChunk.Append(thatCh);
- thatMarker++;
-
- if (thatMarker < s2.Length)
+ else if (span1Len > span2Len)
{
- thatCh = s2[thatMarker];
+ return 1;
}
- }
+ else if (span1Len >= 20) // Number is probably too big for a ulong
+ {
+ // Trim all the first digits that are the same
+ int i = 0;
+ while (i < span1Len && span1[i] == span2[i])
+ {
+ i++;
+ }
+ // If there are no more digits it's the same number
+ if (i == span1Len)
+ {
+ continue;
+ }
- // If both chunks contain numeric characters, sort them numerically
- if (thisNumeric && thatNumeric)
- {
- if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)
- || !int.TryParse(thatChunk.ToString(), out thatNumericChunk))
+ // Only need to compare the most significant digit
+ span1 = span1.Slice(i, 1);
+ span2 = span2.Slice(i, 1);
+ }
+
+ if (!ulong.TryParse(span1, out var num1)
+ || !ulong.TryParse(span2, out var num2))
{
return 0;
}
-
- if (thisNumericChunk < thatNumericChunk)
+ else if (num1 < num2)
{
return -1;
}
-
- if (thisNumericChunk > thatNumericChunk)
+ else if (num1 > num2)
{
return 1;
}
}
else
{
- int result = thisChunk.ToString().CompareTo(thatChunk.ToString());
+ int result = span1.CompareTo(span2, StringComparison.InvariantCulture);
if (result != 0)
{
return result;
}
}
+ } while (pos1 < len1 && pos2 < len2);
- }
-
- return 0;
+ return len1 - len2;
}
+ /// <inheritdoc />
public int Compare(string x, string y)
{
return CompareValues(x, y);
diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
index 35a431fa4..1a661edef 100644
--- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
@@ -11,6 +11,11 @@ namespace MediaBrowser.LocalMetadata.Savers
{
public class PlaylistXmlSaver : BaseXmlSaver
{
+ /// <summary>
+ /// The default file name to use when creating a new playlist.
+ /// </summary>
+ public const string DefaultPlaylistFilename = "playlist.xml";
+
public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
{
if (!item.SupportsLocalMetadata)
@@ -45,7 +50,7 @@ namespace MediaBrowser.LocalMetadata.Savers
return Path.ChangeExtension(itemPath, ".xml");
}
- return Path.Combine(path, "playlist.xml");
+ return Path.Combine(path, DefaultPlaylistFilename);
}
public PlaylistXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger)
diff --git a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
index 6a1a0f090..cc2541f74 100644
--- a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
@@ -1,3 +1,6 @@
+using System;
+using System.Xml.Serialization;
+
namespace MediaBrowser.Model.Configuration
{
/// <summary>
@@ -26,6 +29,24 @@ namespace MediaBrowser.Model.Configuration
public string CachePath { get; set; }
/// <summary>
+ /// Last known version that was ran using the configuration.
+ /// </summary>
+ /// <value>The version from previous run.</value>
+ [XmlIgnore]
+ public Version PreviousVersion { get; set; }
+
+ /// <summary>
+ /// Stringified PreviousVersion to be stored/loaded,
+ /// because System.Version itself isn't xml-serializable
+ /// </summary>
+ /// <value>String value of PreviousVersion</value>
+ public string PreviousVersionStr
+ {
+ get => PreviousVersion?.ToString();
+ set => PreviousVersion = Version.Parse(value);
+ }
+
+ /// <summary>
/// Initializes a new instance of the <see cref="BaseApplicationConfiguration" /> class.
/// </summary>
public BaseApplicationConfiguration()
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index ff431e44c..b3a784de7 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -34,7 +34,7 @@ namespace MediaBrowser.Model.Configuration
public EncodingOptions()
{
DownMixAudioBoost = 2;
- EnableThrottling = true;
+ EnableThrottling = false;
ThrottleDelaySeconds = 180;
EncodingThreadCount = -1;
// This is a DRM device that is almost guaranteed to be there on every intel platform, plus it's the default one in ffmpeg if you don't specify anything
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index bf10108b8..c8f18e69e 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -248,7 +248,7 @@ namespace MediaBrowser.Model.Configuration
PublicHttpsPort = DefaultHttpsPort;
HttpServerPortNumber = DefaultHttpPort;
HttpsPortNumber = DefaultHttpsPort;
- EnableHttps = true;
+ EnableHttps = false;
EnableDashboardResponseCaching = true;
EnableCaseSensitiveItemIds = true;
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index 50570deec..1c84622ac 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -60,6 +60,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api.Tests", "tests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -170,6 +172,10 @@ Global
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {462584F7-5023-4019-9EAC-B98CA458C0A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {462584F7-5023-4019-9EAC-B98CA458C0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -201,5 +207,6 @@ Global
{3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
EndGlobal
diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/fedora-package-x64/Dockerfile
index 05a4ef21f..87120f3a0 100644
--- a/deployment/fedora-package-x64/Dockerfile
+++ b/deployment/fedora-package-x64/Dockerfile
@@ -1,4 +1,4 @@
-FROM fedora:29
+FROM fedora:31
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG PLATFORM_DIR=/jellyfin/deployment/fedora-package-x64
diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi
index 86724b8f4..fada62d98 100644
--- a/deployment/windows/jellyfin.nsi
+++ b/deployment/windows/jellyfin.nsi
@@ -73,7 +73,7 @@ Unicode True
; TODO: Replace with nice Jellyfin Icons
!ifdef UXPATH
!define MUI_ICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Installer Icon
- !define MUI_UNICON "${UXPATH}\branding\NSIS\modern-uninstall.ico" ; Uninstaller Icon
+ !define MUI_UNICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Uninstaller Icon
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "${UXPATH}\branding\NSIS\installer-header.bmp"
diff --git a/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs b/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs
new file mode 100644
index 000000000..929bb92aa
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Linq;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Sorting;
+using Xunit;
+
+namespace Jellyfin.Controller.Tests
+{
+ public class AlphanumComparatorTests
+ {
+ private readonly Random _rng = new Random(42);
+
+ // InlineData is pre-sorted
+ [Theory]
+ [InlineData(null, "", "1", "9", "10", "a", "z")]
+ [InlineData("50F", "100F", "SR9", "SR100")]
+ [InlineData("image-1.jpg", "image-02.jpg", "image-4.jpg", "image-9.jpg", "image-10.jpg", "image-11.jpg", "image-22.jpg")]
+ [InlineData("Hard drive 2GB", "Hard drive 20GB")]
+ [InlineData("b", "e", "è", "ě", "f", "g", "k")]
+ [InlineData("123456789", "123456789a", "abc", "abcd")]
+ [InlineData("12345678912345678912345678913234567891", "123456789123456789123456789132345678912")]
+ [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567891")]
+ [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")]
+ [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")]
+ [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")]
+ public void AlphanumComparatorTest(params string?[] strings)
+ {
+ var copy = (string?[])strings.Clone();
+ if (strings.Length == 2)
+ {
+ var tmp = copy[0];
+ copy[0] = copy[1];
+ copy[1] = tmp;
+ }
+ else
+ {
+ copy.Shuffle(_rng);
+ }
+
+ Array.Sort(copy, new AlphanumComparator());
+ Assert.True(strings.SequenceEqual(copy));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
new file mode 100644
index 000000000..c63f2e8c6
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
+ <PackageReference Include="coverlet.collector" Version="1.2.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
+ </ItemGroup>
+
+</Project>