From 5e10e0ff192de703e8fa18462fa08aa16abac8c4 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 19 Feb 2015 20:57:10 -0500 Subject: replace System.Drawing with ImageMagick --- .../Drawing/ImageHeader.cs | 29 +++++++--------------- 1 file changed, 9 insertions(+), 20 deletions(-) (limited to 'MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs') diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs index e9c67bf48..81d4a786a 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs +++ b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.IO; +using ImageMagickSharp; +using MediaBrowser.Common.IO; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Logging; using System; @@ -61,27 +62,15 @@ namespace MediaBrowser.Server.Implementations.Drawing logger.Info("Failed to read image header for {0}. Doing it the slow way.", path); } - // Buffer to memory stream to avoid image locking file - using (var fs = fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var wand = new MagickWand(path)) { - using (var memoryStream = new MemoryStream()) - { - fs.CopyTo(memoryStream); - - memoryStream.Position = 0; - - // Co it the old fashioned way - using (var b = System.Drawing.Image.FromStream(memoryStream, true, false)) - { - var size = b.Size; + var img = wand.CurrentImage; - return new ImageSize - { - Width = size.Width, - Height = size.Height - }; - } - } + return new ImageSize + { + Width = img.Width, + Height = img.Height + }; } } -- cgit v1.2.3 From 2fc0686c308e74654f4f7ef9ea6cf56fb61b5ff5 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 3 Mar 2015 02:03:17 -0500 Subject: add date content added comparer --- MediaBrowser.Api/Playback/MediaInfoService.cs | 1 + MediaBrowser.Controller/Entities/Audio/Audio.cs | 2 +- MediaBrowser.Controller/Entities/Video.cs | 15 +++-- .../Library/IMediaSourceManager.cs | 18 ++++++ MediaBrowser.Model/Dlna/PlaybackErrorCode.cs | 3 +- .../Drawing/ImageHeader.cs | 3 +- .../Library/MediaSourceManager.cs | 55 +++++++++++++++++ .../MediaBrowser.Server.Implementations.csproj | 1 + .../Photos/BaseDynamicImageProvider.cs | 14 ++++- .../Photos/DynamicImageHelpers.cs | 15 ++++- .../Session/SessionManager.cs | 6 +- .../Sorting/DateLastMediaAddedComparer.cs | 70 ++++++++++++++++++++++ .../Sorting/DatePlayedComparer.cs | 1 - 13 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 MediaBrowser.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs (limited to 'MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs') diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index 77178c8cc..330a8777c 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Api.Playback { [Route("/Items/{Id}/MediaInfo", "GET", Summary = "Gets live playback media info for an item")] + [Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")] public class GetLiveMediaInfo : IReturn { [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 902447999..d868227d9 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -239,7 +239,7 @@ namespace MediaBrowser.Controller.Entities.Audio { Id = i.Id.ToString("N"), Protocol = locationType == LocationType.Remote ? MediaProtocol.Http : MediaProtocol.File, - MediaStreams = MediaSourceManager.GetMediaStreams(new MediaStreamQuery { ItemId = i.Id }).ToList(), + MediaStreams = MediaSourceManager.GetMediaStreams(i.Id).ToList(), Name = i.Name, Path = enablePathSubstituion ? GetMappedPath(i.Path, locationType) : i.Path, RunTimeTicks = i.RunTimeTicks, diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index d4507bc33..a0c3a6cf9 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -420,12 +420,17 @@ namespace MediaBrowser.Controller.Entities return base.GetDeletePaths(); } - public virtual IEnumerable GetMediaStreams() + public IEnumerable GetMediaStreams() { - return MediaSourceManager.GetMediaStreams(new MediaStreamQuery + var mediaSource = GetMediaSources(false) + .FirstOrDefault(); + + if (mediaSource == null) { - ItemId = Id - }); + return new List(); + } + + return mediaSource.MediaStreams; } public virtual MediaStream GetDefaultVideoStream() @@ -474,7 +479,7 @@ namespace MediaBrowser.Controller.Entities private static MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, Video i, MediaSourceType type) { - var mediaStreams = MediaSourceManager.GetMediaStreams(new MediaStreamQuery { ItemId = i.Id }) + var mediaStreams = MediaSourceManager.GetMediaStreams(i.Id) .ToList(); var locationType = i.LocationType; diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 4378bc85d..5d79f613d 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -1,11 +1,29 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; +using System; using System.Collections.Generic; namespace MediaBrowser.Controller.Library { public interface IMediaSourceManager { + /// + /// Gets the media streams. + /// + /// The item identifier. + /// IEnumerable<MediaStream>. + IEnumerable GetMediaStreams(Guid itemId); + /// + /// Gets the media streams. + /// + /// The media source identifier. + /// IEnumerable<MediaStream>. + IEnumerable GetMediaStreams(string mediaSourceId); + /// + /// Gets the media streams. + /// + /// The query. + /// IEnumerable<MediaStream>. IEnumerable GetMediaStreams(MediaStreamQuery query); } } diff --git a/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs index d8d65e91a..4ed412985 100644 --- a/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs +++ b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs @@ -4,6 +4,7 @@ namespace MediaBrowser.Model.Dlna public enum PlaybackErrorCode { NotAllowed = 0, - NoCompatibleStream = 1 + NoCompatibleStream = 1, + RateLimitExceeded = 2 } } diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs index 81d4a786a..6287d0bb8 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs +++ b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs @@ -62,8 +62,9 @@ namespace MediaBrowser.Server.Implementations.Drawing logger.Info("Failed to read image header for {0}. Doing it the slow way.", path); } - using (var wand = new MagickWand(path)) + using (var wand = new MagickWand()) { + wand.PingImage(path); var img = wand.CurrentImage; return new ImageSize diff --git a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs index a45757d13..6ce989b02 100644 --- a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs +++ b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs @@ -1,6 +1,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; +using System; using System.Collections.Generic; using System.Linq; @@ -47,5 +48,59 @@ namespace MediaBrowser.Server.Implementations.Library { return true; } + + public IEnumerable GetMediaStreams(string mediaSourceId) + { + var list = GetMediaStreams(new MediaStreamQuery + { + ItemId = new Guid(mediaSourceId) + }); + + return GetMediaStreamsForItem(list); + } + + public IEnumerable GetMediaStreams(Guid itemId) + { + var list = GetMediaStreams(new MediaStreamQuery + { + ItemId = itemId + }); + + return GetMediaStreamsForItem(list); + } + + private IEnumerable GetMediaStreamsForItem(IEnumerable streams) + { + var list = streams.ToList(); + + var subtitleStreams = list + .Where(i => i.Type == MediaStreamType.Subtitle) + .ToList(); + + if (subtitleStreams.Count > 0) + { + var videoStream = list.FirstOrDefault(i => i.Type == MediaStreamType.Video); + + // This is abitrary but at some point it becomes too slow to extract subtitles on the fly + // We need to learn more about when this is the case vs. when it isn't + const int maxAllowedBitrateForExternalSubtitleStream = 10000000; + + var videoBitrate = videoStream == null ? maxAllowedBitrateForExternalSubtitleStream : videoStream.BitRate ?? maxAllowedBitrateForExternalSubtitleStream; + + foreach (var subStream in subtitleStreams) + { + var supportsExternalStream = StreamSupportsExternalStream(subStream); + + if (supportsExternalStream && videoBitrate >= maxAllowedBitrateForExternalSubtitleStream) + { + supportsExternalStream = false; + } + + subStream.SupportsExternalStream = supportsExternalStream; + } + } + + return list; + } } } diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index a265ffdf1..54df9a86d 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -278,6 +278,7 @@ + diff --git a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs b/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs index e1f98c659..40b85dad1 100644 --- a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs @@ -108,7 +108,12 @@ namespace MediaBrowser.Server.Implementations.Photos protected Task GetThumbCollage(List items) { - return DynamicImageHelpers.GetThumbCollage(items.Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb)).ToList(), + var files = items + .Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb)) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .ToList(); + + return DynamicImageHelpers.GetThumbCollage(files, FileSystem, 1600, 900, @@ -117,7 +122,12 @@ namespace MediaBrowser.Server.Implementations.Photos protected Task GetSquareCollage(List items) { - return DynamicImageHelpers.GetSquareCollage(items.Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb)).ToList(), + var files = items + .Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb)) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .ToList(); + + return DynamicImageHelpers.GetSquareCollage(files, FileSystem, 800, ApplicationPaths); } diff --git a/MediaBrowser.Server.Implementations/Photos/DynamicImageHelpers.cs b/MediaBrowser.Server.Implementations/Photos/DynamicImageHelpers.cs index c2af9cdaf..e7cd2f4d2 100644 --- a/MediaBrowser.Server.Implementations/Photos/DynamicImageHelpers.cs +++ b/MediaBrowser.Server.Implementations/Photos/DynamicImageHelpers.cs @@ -4,6 +4,7 @@ using MediaBrowser.Common.IO; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Photos @@ -15,6 +16,11 @@ namespace MediaBrowser.Server.Implementations.Photos int width, int height, IApplicationPaths appPaths) { + if (files.Any(string.IsNullOrWhiteSpace)) + { + throw new ArgumentException("Empty file found in files list"); + } + if (files.Count < 3) { return await GetSingleImage(files, fileSystem).ConfigureAwait(false); @@ -27,7 +33,7 @@ namespace MediaBrowser.Server.Implementations.Photos int cellHeight = height; var index = 0; - using (var wand = new MagickWand(width, height, "transparent")) + using (var wand = new MagickWand(width, height, new PixelWand(ColorName.None, 1))) { for (var row = 0; row < rows; row++) { @@ -57,6 +63,11 @@ namespace MediaBrowser.Server.Implementations.Photos IFileSystem fileSystem, int size, IApplicationPaths appPaths) { + if (files.Any(string.IsNullOrWhiteSpace)) + { + throw new ArgumentException("Empty file found in files list"); + } + if (files.Count < 4) { return await GetSingleImage(files, fileSystem).ConfigureAwait(false); @@ -68,7 +79,7 @@ namespace MediaBrowser.Server.Implementations.Photos int singleSize = size / 2; var index = 0; - using (var wand = new MagickWand(size, size, "transparent")) + using (var wand = new MagickWand(size, size, new PixelWand(ColorName.None, 1))) { for (var row = 0; row < rows; row++) { diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 3ffbf5cb9..8eef8536a 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -1579,11 +1579,7 @@ namespace MediaBrowser.Server.Implementations.Session if (!string.IsNullOrWhiteSpace(mediaSourceId)) { - info.MediaStreams = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery - { - ItemId = new Guid(mediaSourceId) - - }).ToList(); + info.MediaStreams = _mediaSourceManager.GetMediaStreams(mediaSourceId).ToList(); } return info; diff --git a/MediaBrowser.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/MediaBrowser.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs new file mode 100644 index 000000000..68cd44ec9 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs @@ -0,0 +1,70 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Querying; +using System; +using System.Linq; + +namespace MediaBrowser.Server.Implementations.Sorting +{ + public class DateLastMediaAddedComparer : IUserBaseItemComparer + { + /// + /// Gets or sets the user. + /// + /// The user. + public User User { get; set; } + + /// + /// Gets or sets the user manager. + /// + /// The user manager. + public IUserManager UserManager { get; set; } + + /// + /// Gets or sets the user data repository. + /// + /// The user data repository. + public IUserDataManager UserDataRepository { get; set; } + + /// + /// Compares the specified x. + /// + /// The x. + /// The y. + /// System.Int32. + public int Compare(BaseItem x, BaseItem y) + { + return GetDate(x).CompareTo(GetDate(y)); + } + + /// + /// Gets the date. + /// + /// The x. + /// DateTime. + private DateTime GetDate(BaseItem x) + { + var folder = x as Folder; + + if (folder != null) + { + return folder.GetRecursiveChildren(User, i => !i.IsFolder) + .Select(i => i.DateCreated) + .OrderByDescending(i => i) + .FirstOrDefault(); + } + + return x.DateCreated; + } + + /// + /// Gets the name. + /// + /// The name. + public string Name + { + get { return ItemSortBy.DateLastContentAdded; } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Sorting/DatePlayedComparer.cs b/MediaBrowser.Server.Implementations/Sorting/DatePlayedComparer.cs index 7605a7a50..c881591be 100644 --- a/MediaBrowser.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -1,6 +1,5 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; using System; -- cgit v1.2.3 From 23a062103a90caa70963f12fd06b035b8e122305 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 6 Mar 2015 12:50:14 -0500 Subject: move image magick files --- .../Drawing/ImageHeader.cs | 28 +----- .../Drawing/ImageProcessor.cs | 32 +++++- MediaBrowser.ServerApplication/MainStartup.cs | 2 +- .../MediaBrowser.ServerApplication.csproj | 109 +++++++++++---------- 4 files changed, 88 insertions(+), 83 deletions(-) (limited to 'MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs') diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs index 6287d0bb8..7117482c8 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs +++ b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs @@ -1,5 +1,4 @@ -using ImageMagickSharp; -using MediaBrowser.Common.IO; +using MediaBrowser.Common.IO; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Logging; using System; @@ -47,32 +46,13 @@ namespace MediaBrowser.Server.Implementations.Drawing /// The image was of an unrecognised format. public static ImageSize GetDimensions(string path, ILogger logger, IFileSystem fileSystem) { - try + using (var fs = File.OpenRead(path)) { - using (var fs = File.OpenRead(path)) + using (var binaryReader = new BinaryReader(fs)) { - using (var binaryReader = new BinaryReader(fs)) - { - return GetDimensions(binaryReader); - } + return GetDimensions(binaryReader); } } - catch - { - logger.Info("Failed to read image header for {0}. Doing it the slow way.", path); - } - - using (var wand = new MagickWand()) - { - wand.PingImage(path); - var img = wand.CurrentImage; - - return new ImageSize - { - Width = img.Width, - Height = img.Height - }; - } } /// diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs index 64c812e1b..80cfd7f3e 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs +++ b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs @@ -203,7 +203,7 @@ namespace MediaBrowser.Server.Implementations.Drawing try { CheckDisposed(); - + var newWidth = Convert.ToInt32(newSize.Width); var newHeight = Convert.ToInt32(newSize.Height); @@ -329,7 +329,7 @@ namespace MediaBrowser.Server.Implementations.Drawing try { Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath)); - + using (var wand = new MagickWand(originalImagePath)) { wand.CurrentImage.TrimImage(10); @@ -450,12 +450,34 @@ namespace MediaBrowser.Server.Implementations.Drawing /// ImageSize. private ImageSize GetImageSizeInternal(string path) { - CheckDisposed(); - var size = ImageHeader.GetDimensions(path, _logger, _fileSystem); + ImageSize size; + + try + { + size = ImageHeader.GetDimensions(path, _logger, _fileSystem); + } + catch + { + _logger.Info("Failed to read image header for {0}. Doing it the slow way.", path); + + CheckDisposed(); + + using (var wand = new MagickWand()) + { + wand.PingImage(path); + var img = wand.CurrentImage; + + size = new ImageSize + { + Width = img.Width, + Height = img.Height + }; + } + } StartSaveImageSizeTimer(); - return new ImageSize { Width = size.Width, Height = size.Height }; + return size; } private readonly Timer _saveImageSizeTimer; diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index 4bf51bc6b..598ec1fd7 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.ServerApplication var applicationPath = currentProcess.MainModule.FileName; - Wand.SetMagickCoderModulePath(Path.Combine(Path.GetDirectoryName(applicationPath), "ImageMagickCoders", "x86")); + //Wand.SetMagickCoderModulePath(Path.Combine(Path.GetDirectoryName(applicationPath), "ImageMagickCoders", "x86")); var appPaths = CreateApplicationPaths(applicationPath, _isRunningAsService); var logManager = new NlogManager(appPaths.LogDirectoryPath, "server"); diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index 7c23f9341..fb889e4f2 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -239,163 +239,163 @@ PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest @@ -455,6 +455,9 @@ MediaBrowser.XbmcMetadata + + + -- cgit v1.2.3