diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 7c3959f6e8..c00f76f22c 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -19,12 +19,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// The encoder path. string EncoderPath { get; } - /// - /// Gets the version. - /// - /// The version. - string Version { get; } - /// /// Supportses the decoder. /// @@ -134,5 +128,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// The path. /// System.String. string EscapeSubtitleFilterPath(string path); + + void Init(); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 399fdead9c..39a2338568 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -21,6 +21,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Common.Configuration; namespace MediaBrowser.MediaEncoding.Encoder { @@ -64,8 +66,6 @@ namespace MediaBrowser.MediaEncoding.Encoder public string FFProbePath { get; private set; } - public string Version { get; private set; } - protected readonly IServerConfigurationManager ConfigurationManager; protected readonly IFileSystem FileSystem; protected readonly ILiveTvManager LiveTvManager; @@ -77,12 +77,12 @@ namespace MediaBrowser.MediaEncoding.Encoder protected readonly Func MediaSourceManager; private readonly List _runningProcesses = new List(); + private readonly bool _hasExternalEncoder; - public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func subtitleEncoder, Func mediaSourceManager) + public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, bool hasExternalEncoder, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func subtitleEncoder, Func mediaSourceManager) { _logger = logger; _jsonSerializer = jsonSerializer; - Version = version; ConfigurationManager = configurationManager; FileSystem = fileSystem; LiveTvManager = liveTvManager; @@ -94,6 +94,95 @@ namespace MediaBrowser.MediaEncoding.Encoder MediaSourceManager = mediaSourceManager; FFProbePath = ffProbePath; FFMpegPath = ffMpegPath; + + _hasExternalEncoder = hasExternalEncoder; + } + + public void Init() + { + ConfigureEncoderPaths(); + + if (_hasExternalEncoder) + { + LogPaths(); + return; + } + + // If the path was passed in, save it into config now. + var encodingOptions = GetEncodingOptions(); + var appPath = encodingOptions.EncoderAppPath; + if (!string.IsNullOrWhiteSpace(FFMpegPath) && !string.Equals(FFMpegPath, appPath, StringComparison.Ordinal)) + { + encodingOptions.EncoderAppPath = FFMpegPath; + ConfigurationManager.SaveConfiguration("encoding", encodingOptions); + } + } + + private void ConfigureEncoderPaths() + { + if (_hasExternalEncoder) + { + return; + } + + var appPath = GetEncodingOptions().EncoderAppPath; + + if (string.IsNullOrWhiteSpace(appPath)) + { + appPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg"); + } + + if (!string.IsNullOrWhiteSpace(appPath)) + { + if (Directory.Exists(appPath)) + { + SetPathsFromDirectory(appPath); + } + + else if (File.Exists(appPath)) + { + FFMpegPath = appPath; + + SetProbePathFromEncoderPath(appPath); + } + } + + LogPaths(); + } + + private void SetPathsFromDirectory(string path) + { + // Since we can't predict the file extension, first try directly within the folder + // If that doesn't pan out, then do a recursive search + var files = Directory.GetFiles(path); + + FFMpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase)); + FFProbePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrWhiteSpace(FFMpegPath) || !File.Exists(FFMpegPath)) + { + files = Directory.GetFiles(path, "*", SearchOption.AllDirectories); + + FFMpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase)); + SetProbePathFromEncoderPath(FFMpegPath); + } + } + + private void SetProbePathFromEncoderPath(string appPath) + { + FFProbePath = Directory.GetFiles(Path.GetDirectoryName(appPath)) + .FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase)); + } + + private void LogPaths() + { + _logger.Info("FFMpeg: {0}", FFMpegPath ?? "not found"); + _logger.Info("FFProbe: {0}", FFProbePath ?? "not found"); + } + + private EncodingOptions GetEncodingOptions() + { + return ConfigurationManager.GetConfiguration("encoding"); } private List _encoders = new List(); diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index d33cf55775..91d28a2969 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -9,6 +9,7 @@ namespace MediaBrowser.Model.Configuration public bool EnableThrottling { get; set; } public int ThrottleDelaySeconds { get; set; } public string HardwareAccelerationType { get; set; } + public string EncoderAppPath { get; set; } public EncodingOptions() { diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 6b54a90d44..73d5961f67 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -152,6 +152,8 @@ namespace MediaBrowser.Model.System /// true if [supports automatic run at startup]; otherwise, false. public bool SupportsAutoRunAtStartup { get; set; } + public bool HasExternalEncoder { get; set; } + /// /// Initializes a new instance of the class. /// diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 388dfd515d..a4cf90e5b0 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -1276,26 +1276,22 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.Artists = hasArtist.Artists; - dto.ArtistItems = hasArtist - .Artists + var artistItems = _libraryManager.GetArtists(new InternalItemsQuery + { + EnableTotalRecordCount = false, + ItemIds = new[] { item.Id.ToString("N") } + }); + + dto.ArtistItems = artistItems.Items .Select(i => { - try + var artist = i.Item1; + return new NameIdPair { - var artist = _libraryManager.GetArtist(i); - return new NameIdPair - { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - } - catch (Exception ex) - { - _logger.ErrorException("Error getting artist", ex); - return null; - } + Name = artist.Name, + Id = artist.Id.ToString("N") + }; }) - .Where(i => i != null) .ToList(); } @@ -1304,26 +1300,22 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); - dto.AlbumArtists = hasAlbumArtist - .AlbumArtists + var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery + { + EnableTotalRecordCount = false, + ItemIds = new[] { item.Id.ToString("N") } + }); + + dto.ArtistItems = artistItems.Items .Select(i => { - try + var artist = i.Item1; + return new NameIdPair { - var artist = _libraryManager.GetArtist(i); - return new NameIdPair - { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - } - catch (Exception ex) - { - _logger.ErrorException("Error getting album artist", ex); - return null; - } + Name = artist.Name, + Id = artist.Id.ToString("N") + }; }) - .Where(i => i != null) .ToList(); } @@ -1604,7 +1596,7 @@ namespace MediaBrowser.Server.Implementations.Dto { IsFolder = false, Recursive = true, - ExcludeLocationTypes = new[] {LocationType.Virtual}, + ExcludeLocationTypes = new[] { LocationType.Virtual }, User = user }).ConfigureAwait(false); diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index aabe704c7a..883864156c 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -944,9 +944,7 @@ namespace MediaBrowser.Server.Implementations.Library private T CreateItemByName(string path, string name) where T : BaseItem, new() { - var isArtist = typeof(T) == typeof(MusicArtist); - - if (isArtist) + if (typeof(T) == typeof(MusicArtist)) { var existing = GetItemList(new InternalItemsQuery { @@ -1277,11 +1275,6 @@ namespace MediaBrowser.Server.Implementations.Library return item; } - private bool EnableCaching - { - get { return false; } - } - public IEnumerable GetItemList(InternalItemsQuery query) { if (query.User != null) @@ -1289,14 +1282,7 @@ namespace MediaBrowser.Server.Implementations.Library AddUserToQuery(query, query.User); } - if (!EnableCaching) - { - return ItemRepository.GetItemList(query); - } - - var result = ItemRepository.GetItemIdsList(query); - - return result.Select(GetItemById).Where(i => i != null); + return ItemRepository.GetItemList(query); } public QueryResult QueryItems(InternalItemsQuery query) @@ -1426,12 +1412,7 @@ namespace MediaBrowser.Server.Implementations.Library SetTopParentIdsOrAncestors(query, parents); - if (!EnableCaching) - { - return ItemRepository.GetItemList(query); - } - - return GetItemIds(query).Select(GetItemById).Where(i => i != null); + return ItemRepository.GetItemList(query); } public QueryResult GetItemsResult(InternalItemsQuery query) @@ -1453,31 +1434,12 @@ namespace MediaBrowser.Server.Implementations.Library if (query.EnableTotalRecordCount) { - if (!EnableCaching) - { - return ItemRepository.GetItems(query); - } - - var initialResult = ItemRepository.GetItemIds(query); - - return new QueryResult - { - TotalRecordCount = initialResult.TotalRecordCount, - Items = initialResult.Items.Select(GetItemById).Where(i => i != null).ToArray() - }; - } - - if (!EnableCaching) - { - return new QueryResult - { - Items = ItemRepository.GetItemList(query).ToArray() - }; + return ItemRepository.GetItems(query); } return new QueryResult { - Items = ItemRepository.GetItemIdsList(query).Select(GetItemById).Where(i => i != null).ToArray() + Items = ItemRepository.GetItemList(query).ToArray() }; } @@ -1499,7 +1461,7 @@ namespace MediaBrowser.Server.Implementations.Library return true; } - _logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name); + //_logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name); return false; })) diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 9f80877f63..29c4a43d38 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -140,10 +140,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); + var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); + var channels = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }, - SortBy = new[] { ItemSortBy.SortName } + SortBy = new[] { ItemSortBy.SortName }, + TopParentIds = new[] { topFolder.Id.ToString("N") } }).Cast(); @@ -891,6 +894,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); + var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, @@ -907,7 +912,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv Limit = query.Limit, SortBy = query.SortBy, SortOrder = query.SortOrder ?? SortOrder.Ascending, - EnableTotalRecordCount = query.EnableTotalRecordCount + EnableTotalRecordCount = query.EnableTotalRecordCount, + TopParentIds = new[] { topFolder.Id.ToString("N") } }; if (query.HasAired.HasValue) @@ -939,6 +945,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = _userManager.GetUserById(query.UserId); + var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, @@ -947,7 +955,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv IsSports = query.IsSports, IsKids = query.IsKids, EnableTotalRecordCount = query.EnableTotalRecordCount, - SortBy = new[] { ItemSortBy.StartDate } + SortBy = new[] { ItemSortBy.StartDate }, + TopParentIds = new[] { topFolder.Id.ToString("N") } }; if (query.Limit.HasValue) @@ -1905,7 +1914,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv MaxStartDate = now, MinEndDate = now, Limit = channelIds.Length, - SortBy = new[] { "StartDate" } + SortBy = new[] { "StartDate" }, + TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Result.Id.ToString("N") } }, new string[] { }).ToList(); diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs index e8b39a1fe3..ed5a64b8c8 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -155,12 +155,14 @@ namespace MediaBrowser.Server.Implementations.Persistence "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", "create index if not exists idx_AncestorIds2 on AncestorIds(AncestorIdText)", - "create table if not exists UserDataKeys (ItemId GUID, UserDataKey TEXT, PRIMARY KEY (ItemId, UserDataKey))", - "create index if not exists idx_UserDataKeys1 on UserDataKeys(ItemId)", + "create table if not exists UserDataKeys (ItemId GUID, UserDataKey TEXT Priority INT, PRIMARY KEY (ItemId, UserDataKey))", + //"create index if not exists idx_UserDataKeys1 on UserDataKeys(ItemId)", + "create index if not exists idx_UserDataKeys2 on UserDataKeys(ItemId,Priority)", "create table if not exists ItemValues (ItemId GUID, Type INT, Value TEXT, CleanValue TEXT)", - "create index if not exists idx_ItemValues on ItemValues(ItemId)", + //"create index if not exists idx_ItemValues on ItemValues(ItemId)", "create index if not exists idx_ItemValues2 on ItemValues(ItemId,Type)", + "create index if not exists idx_ItemValues3 on ItemValues(ItemId,Type,CleanValue)", "create table if not exists ProviderIds (ItemId GUID, Name TEXT, Value TEXT, PRIMARY KEY (ItemId, Name))", "create index if not exists Idx_ProviderIds on ProviderIds(ItemId)", @@ -169,11 +171,10 @@ namespace MediaBrowser.Server.Implementations.Persistence "create index if not exists idx_Images on Images(ItemId)", "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", - "create index if not exists idxPeopleItemId on People(ItemId)", + "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", "create index if not exists idxPeopleName on People(Name)", "create table if not exists "+ChaptersTableName+" (ItemId GUID, ChapterIndex INT, StartPositionTicks BIGINT, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - "create index if not exists idx_"+ChaptersTableName+"1 on "+ChaptersTableName+"(ItemId)", createMediaStreamsTableCommand, "create index if not exists idx_mediastreams1 on mediastreams(ItemId)", @@ -270,13 +271,23 @@ namespace MediaBrowser.Server.Implementations.Persistence { "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", "create index if not exists idx_GuidType on TypedBaseItems(Guid,Type)", + "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", "create index if not exists idx_Type on TypedBaseItems(Type)", "create index if not exists idx_TopParentId on TypedBaseItems(TopParentId)", "create index if not exists idx_TypeTopParentId on TypedBaseItems(Type,TopParentId)", + + // used by movie suggestions + "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", "create index if not exists idx_TypeTopParentId2 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem)", "create index if not exists idx_TypeTopParentId3 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem)", "create index if not exists idx_TypeTopParentId4 on TypedBaseItems(TopParentId,Type,IsVirtualItem)", - "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)" + "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", + "create index if not exists idx_TypeTopParentId6 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey)", + + // latest items + "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", + "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", + "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey)" }; _connection.RunQueries(postQueries, Logger); diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index 7341f56cbd..1130d3a11b 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -323,6 +323,8 @@ namespace MediaBrowser.Server.Startup.Common await base.RunStartupTasks().ConfigureAwait(false); + InitMediaEncoder(); + Logger.Info("ServerId: {0}", SystemId); Logger.Info("Core startup complete"); HttpServer.GlobalResponse = null; @@ -344,6 +346,20 @@ namespace MediaBrowser.Server.Startup.Common LogManager.RemoveConsoleOutput(); } + private void InitMediaEncoder() + { + MediaEncoder.Init(); + + Task.Run(() => + { + var result = new FFmpegValidator(Logger, ApplicationPaths, FileSystemManager).Validate(MediaEncoder.EncoderPath); + + var mediaEncoder = (MediaEncoder) MediaEncoder; + mediaEncoder.SetAvailableDecoders(result.Item1); + mediaEncoder.SetAvailableEncoders(result.Item2); + }); + } + public override Task Init(IProgress progress) { HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; @@ -634,11 +650,13 @@ namespace MediaBrowser.Server.Startup.Common var info = await new FFMpegLoader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager, NativeApp.Environment, NativeApp.GetType().Assembly, NativeApp.GetFfmpegInstallInfo()) .GetFFMpegInfo(NativeApp.Environment, _startupOptions, progress).ConfigureAwait(false); + _hasExternalEncoder = string.Equals(info.Version, "custom", StringComparison.OrdinalIgnoreCase); + var mediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), JsonSerializer, info.EncoderPath, info.ProbePath, - info.Version, + _hasExternalEncoder, ServerConfigurationManager, FileSystemManager, LiveTvManager, @@ -651,14 +669,6 @@ namespace MediaBrowser.Server.Startup.Common MediaEncoder = mediaEncoder; RegisterSingleInstance(MediaEncoder); - - Task.Run(() => - { - var result = new FFmpegValidator(Logger, ApplicationPaths, FileSystemManager).Validate(info); - - mediaEncoder.SetAvailableDecoders(result.Item1); - mediaEncoder.SetAvailableEncoders(result.Item2); - }); } /// @@ -1094,6 +1104,7 @@ namespace MediaBrowser.Server.Startup.Common } } + private bool _hasExternalEncoder; /// /// Gets the system status. /// @@ -1133,7 +1144,8 @@ namespace MediaBrowser.Server.Startup.Common SupportsRunningAsService = SupportsRunningAsService, ServerName = FriendlyName, LocalAddress = localAddress, - SupportsLibraryMonitor = SupportsLibraryMonitor + SupportsLibraryMonitor = SupportsLibraryMonitor, + HasExternalEncoder = _hasExternalEncoder }; } diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFmpegValidator.cs b/MediaBrowser.Server.Startup.Common/FFMpeg/FFmpegValidator.cs index 0ae021407b..d92dc1b965 100644 --- a/MediaBrowser.Server.Startup.Common/FFMpeg/FFmpegValidator.cs +++ b/MediaBrowser.Server.Startup.Common/FFMpeg/FFmpegValidator.cs @@ -21,13 +21,10 @@ namespace MediaBrowser.Server.Startup.Common.FFMpeg _fileSystem = fileSystem; } - public Tuple,List> Validate(FFMpegInfo info) + public Tuple,List> Validate(string encoderPath) { - _logger.Info("FFMpeg: {0}", info.EncoderPath); - _logger.Info("FFProbe: {0}", info.ProbePath); - - var decoders = GetDecoders(info.EncoderPath); - var encoders = GetEncoders(info.EncoderPath); + var decoders = GetDecoders(encoderPath); + var encoders = GetEncoders(encoderPath); return new Tuple, List>(decoders, encoders); }