diff --git a/Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs index 5bd18cb808..700d04c4d3 100644 --- a/Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs @@ -736,10 +736,10 @@ namespace Emby.Common.Implementations.HttpClientManager { if (options.LogErrors) { - _logger.ErrorException("Error getting response from " + options.Url, ex); + _logger.ErrorException("Error " + webException.Status + " getting response from " + options.Url, webException); } - var exception = new HttpException(ex.Message, ex); + var exception = new HttpException(webException.Message, webException); var response = webException.Response as HttpWebResponse; if (response != null) @@ -752,6 +752,15 @@ namespace Emby.Common.Implementations.HttpClientManager } } + if (!exception.StatusCode.HasValue) + { + if (webException.Status == WebExceptionStatus.NameResolutionFailure || + webException.Status == WebExceptionStatus.ConnectFailure) + { + exception.IsTimedOut = true; + } + } + return exception; } diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index e93ee5990d..9345a1df71 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -26,6 +26,7 @@ using System.Xml; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Xml; @@ -482,6 +483,12 @@ namespace Emby.Dlna.ContentDirectory return GetMusicArtistItems(item, null, user, sort, startIndex, limit); } + var collectionFolder = item as ICollectionFolder; + if (collectionFolder != null && string.Equals(CollectionType.Music, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase)) + { + return GetMusicFolders(item, user, stubType, sort, startIndex, limit); + } + if (stubType.HasValue) { if (stubType.Value == StubType.People) @@ -518,7 +525,7 @@ namespace Emby.Dlna.ContentDirectory StartIndex = startIndex, User = user, IsMissing = false, - PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows, CollectionType.Music }, + PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows }, ExcludeItemTypes = new[] { typeof(Game).Name, typeof(Book).Name }, IsPlaceHolder = false, DtoOptions = GetDtoOptions() @@ -531,6 +538,278 @@ namespace Emby.Dlna.ContentDirectory return ToResult(queryResult); } + private QueryResult GetMusicFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) + { + var query = new InternalItemsQuery(user) + { + StartIndex = startIndex, + Limit = limit + }; + SetSorting(query, sort, false); + + if (stubType.HasValue && stubType.Value == StubType.Latest) + { + return GetMusicLatest(item, user, query); + } + + if (stubType.HasValue && stubType.Value == StubType.Playlists) + { + return GetMusicPlaylists(item, user, query); + } + + if (stubType.HasValue && stubType.Value == StubType.Albums) + { + return GetMusicAlbums(item, user, query); + } + + if (stubType.HasValue && stubType.Value == StubType.Artists) + { + return GetMusicArtists(item, user, query); + } + + if (stubType.HasValue && stubType.Value == StubType.AlbumArtists) + { + return GetMusicAlbumArtists(item, user, query); + } + + if (stubType.HasValue && stubType.Value == StubType.FavoriteAlbums) + { + return GetFavoriteAlbums(item, user, query); + } + + if (stubType.HasValue && stubType.Value == StubType.FavoriteArtists) + { + return GetFavoriteArtists(item, user, query); + } + + if (stubType.HasValue && stubType.Value == StubType.FavoriteSongs) + { + return GetFavoriteSongs(item, user, query); + } + + if (stubType.HasValue && stubType.Value == StubType.Songs) + { + return GetMusicSongs(item, user, query); + } + + if (stubType.HasValue && stubType.Value == StubType.Genres) + { + return GetMusicGenres(item, user, query); + } + + var list = new List(); + + list.Add(new ServerItem(item) + { + StubType = StubType.Latest + }); + + list.Add(new ServerItem(item) + { + StubType = StubType.Playlists + }); + + list.Add(new ServerItem(item) + { + StubType = StubType.Albums + }); + + list.Add(new ServerItem(item) + { + StubType = StubType.AlbumArtists + }); + + list.Add(new ServerItem(item) + { + StubType = StubType.Artists + }); + + list.Add(new ServerItem(item) + { + StubType = StubType.Songs + }); + + list.Add(new ServerItem(item) + { + StubType = StubType.Genres + }); + + list.Add(new ServerItem(item) + { + StubType = StubType.FavoriteArtists + }); + + list.Add(new ServerItem(item) + { + StubType = StubType.FavoriteAlbums + }); + + list.Add(new ServerItem(item) + { + StubType = StubType.FavoriteSongs + }); + + return new QueryResult + { + Items = list.ToArray(), + TotalRecordCount = list.Count + }; + } + + private QueryResult GetMusicAlbums(BaseItem parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name }; + + var result = _libraryManager.GetItemsResult(query); + + return ToResult(result); + } + + private QueryResult GetMusicSongs(BaseItem parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + + query.IncludeItemTypes = new[] { typeof(Audio).Name }; + + var result = _libraryManager.GetItemsResult(query); + + return ToResult(result); + } + + private QueryResult GetFavoriteSongs(BaseItem parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.IsFavorite = true; + query.IncludeItemTypes = new[] { typeof(Audio).Name }; + + var result = _libraryManager.GetItemsResult(query); + + return ToResult(result); + } + + private QueryResult GetFavoriteAlbums(BaseItem parent, User user, InternalItemsQuery query) + { + query.Recursive = true; + query.Parent = parent; + query.SetUser(user); + query.IsFavorite = true; + query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name }; + + var result = _libraryManager.GetItemsResult(query); + + return ToResult(result); + } + + private QueryResult GetMusicGenres(BaseItem parent, User user, InternalItemsQuery query) + { + var genresResult = _libraryManager.GetMusicGenres(new InternalItemsQuery(user) + { + AncestorIds = new[] { parent.Id.ToString("N") }, + StartIndex = query.StartIndex, + Limit = query.Limit + }); + + var result = new QueryResult + { + TotalRecordCount = genresResult.TotalRecordCount, + Items = genresResult.Items.Select(i => i.Item1).ToArray() + }; + + return ToResult(result); + } + + private QueryResult GetMusicAlbumArtists(BaseItem parent, User user, InternalItemsQuery query) + { + var artists = _libraryManager.GetAlbumArtists(new InternalItemsQuery(user) + { + AncestorIds = new[] { parent.Id.ToString("N") }, + StartIndex = query.StartIndex, + Limit = query.Limit + }); + + var result = new QueryResult + { + TotalRecordCount = artists.TotalRecordCount, + Items = artists.Items.Select(i => i.Item1).ToArray() + }; + + return ToResult(result); + } + + private QueryResult GetMusicArtists(BaseItem parent, User user, InternalItemsQuery query) + { + var artists = _libraryManager.GetArtists(new InternalItemsQuery(user) + { + AncestorIds = new[] { parent.Id.ToString("N") }, + StartIndex = query.StartIndex, + Limit = query.Limit + }); + + var result = new QueryResult + { + TotalRecordCount = artists.TotalRecordCount, + Items = artists.Items.Select(i => i.Item1).ToArray() + }; + + return ToResult(result); + } + + private QueryResult GetFavoriteArtists(BaseItem parent, User user, InternalItemsQuery query) + { + var artists = _libraryManager.GetArtists(new InternalItemsQuery(user) + { + AncestorIds = new[] { parent.Id.ToString("N") }, + StartIndex = query.StartIndex, + Limit = query.Limit, + IsFavorite = true + }); + + var result = new QueryResult + { + TotalRecordCount = artists.TotalRecordCount, + Items = artists.Items.Select(i => i.Item1).ToArray() + }; + + return ToResult(result); + } + + private QueryResult GetMusicPlaylists(BaseItem parent, User user, InternalItemsQuery query) + { + query.Parent = null; + query.IncludeItemTypes = new[] { typeof(Playlist).Name }; + query.SetUser(user); + query.Recursive = true; + + var result = _libraryManager.GetItemsResult(query); + + return ToResult(result); + } + + private QueryResult GetMusicLatest(BaseItem parent, User user, InternalItemsQuery query) + { + query.SortBy = new string[] { }; + + var items = _userViewManager.GetLatestItems(new LatestItemsQuery + { + UserId = user.Id.ToString("N"), + Limit = 50, + IncludeItemTypes = new[] { typeof(Audio).Name }, + ParentId = parent == null ? null : parent.Id.ToString("N"), + GroupItems = true + + }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToList(); + + return ToResult(items); + } + private QueryResult GetMusicArtistItems(BaseItem item, Guid? parentId, User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) @@ -571,6 +850,19 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } + private QueryResult ToResult(List result) + { + var serverItems = result + .Select(i => new ServerItem(i)) + .ToArray(); + + return new QueryResult + { + TotalRecordCount = result.Count, + Items = serverItems + }; + } + private QueryResult ToResult(QueryResult result) { var serverItems = result @@ -660,6 +952,56 @@ namespace Emby.Dlna.ContentDirectory stubType = StubType.People; id = id.Split(new[] { '_' }, 2)[1]; } + else if (id.StartsWith("latest_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.Latest; + id = id.Split(new[] { '_' }, 2)[1]; + } + else if (id.StartsWith("playlists_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.Playlists; + id = id.Split(new[] { '_' }, 2)[1]; + } + else if (id.StartsWith("Albums_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.Albums; + id = id.Split(new[] { '_' }, 2)[1]; + } + else if (id.StartsWith("AlbumArtists_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.AlbumArtists; + id = id.Split(new[] { '_' }, 2)[1]; + } + else if (id.StartsWith("Artists_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.Artists; + id = id.Split(new[] { '_' }, 2)[1]; + } + else if (id.StartsWith("Genres_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.Genres; + id = id.Split(new[] { '_' }, 2)[1]; + } + else if (id.StartsWith("Songs_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.Songs; + id = id.Split(new[] { '_' }, 2)[1]; + } + else if (id.StartsWith("FavoriteAlbums_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.FavoriteAlbums; + id = id.Split(new[] { '_' }, 2)[1]; + } + else if (id.StartsWith("FavoriteArtists_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.FavoriteArtists; + id = id.Split(new[] { '_' }, 2)[1]; + } + else if (id.StartsWith("FavoriteSongs_", StringComparison.OrdinalIgnoreCase)) + { + stubType = StubType.FavoriteSongs; + id = id.Split(new[] { '_' }, 2)[1]; + } if (Guid.TryParse(id, out itemId)) { @@ -696,6 +1038,16 @@ namespace Emby.Dlna.ContentDirectory public enum StubType { Folder = 0, - People = 1 + People = 1, + Latest = 2, + Playlists = 3, + Albums = 4, + AlbumArtists = 5, + Artists = 6, + Songs = 7, + Genres = 8, + FavoriteSongs = 9, + FavoriteArtists = 10, + FavoriteAlbums = 11 } } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 3344bfcfe3..d2a160cf78 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -399,6 +399,46 @@ namespace Emby.Dlna.Didl } return _localization.GetLocalizedString("HeaderPeople"); } + if (itemStubType.HasValue && itemStubType.Value == StubType.Latest) + { + return _localization.GetLocalizedString("ViewTypeMusicLatest"); + } + if (itemStubType.HasValue && itemStubType.Value == StubType.Playlists) + { + return _localization.GetLocalizedString("ViewTypeMusicPlaylists"); + } + if (itemStubType.HasValue && itemStubType.Value == StubType.AlbumArtists) + { + return _localization.GetLocalizedString("ViewTypeMusicAlbumArtists"); + } + if (itemStubType.HasValue && itemStubType.Value == StubType.Albums) + { + return _localization.GetLocalizedString("ViewTypeMusicAlbums"); + } + if (itemStubType.HasValue && itemStubType.Value == StubType.Artists) + { + return _localization.GetLocalizedString("ViewTypeMusicArtists"); + } + if (itemStubType.HasValue && itemStubType.Value == StubType.Songs) + { + return _localization.GetLocalizedString("ViewTypeMusicSongs"); + } + if (itemStubType.HasValue && itemStubType.Value == StubType.Genres) + { + return _localization.GetLocalizedString("ViewTypeTvGenres"); + } + if (itemStubType.HasValue && itemStubType.Value == StubType.FavoriteAlbums) + { + return _localization.GetLocalizedString("ViewTypeMusicFavoriteAlbums"); + } + if (itemStubType.HasValue && itemStubType.Value == StubType.FavoriteArtists) + { + return _localization.GetLocalizedString("ViewTypeMusicFavoriteArtists"); + } + if (itemStubType.HasValue && itemStubType.Value == StubType.FavoriteSongs) + { + return _localization.GetLocalizedString("ViewTypeMusicFavoriteSongs"); + } var episode = item as Episode; var season = context as Season; diff --git a/Emby.Dlna/Eventing/EventManager.cs b/Emby.Dlna/Eventing/EventManager.cs index cf2c8d9956..0516585ae0 100644 --- a/Emby.Dlna/Eventing/EventManager.cs +++ b/Emby.Dlna/Eventing/EventManager.cs @@ -26,32 +26,34 @@ namespace Emby.Dlna.Eventing _logger = logger; } - public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, int? timeoutSeconds) + public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string requestedTimeoutString) { - var timeout = timeoutSeconds ?? 300; - var subscription = GetSubscription(subscriptionId, true); - _logger.Debug("Renewing event subscription for {0} with timeout of {1} to {2}", - subscription.NotificationType, - timeout, - subscription.CallbackUrl); + // Remove logging for now because some devices are sending this very frequently + // TODO re-enable with dlna debug logging setting + //_logger.Debug("Renewing event subscription for {0} with timeout of {1} to {2}", + // subscription.NotificationType, + // timeout, + // subscription.CallbackUrl); - subscription.TimeoutSeconds = timeout; + subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300; subscription.SubscriptionTime = DateTime.UtcNow; - return GetEventSubscriptionResponse(subscriptionId, timeout); + return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, subscription.TimeoutSeconds); } - public EventSubscriptionResponse CreateEventSubscription(string notificationType, int? timeoutSeconds, string callbackUrl) + public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl) { - var timeout = timeoutSeconds ?? 300; + var timeout = ParseTimeout(requestedTimeoutString) ?? 300; var id = "uuid:" + Guid.NewGuid().ToString("N"); - _logger.Debug("Creating event subscription for {0} with timeout of {1} to {2}", - notificationType, - timeout, - callbackUrl); + // Remove logging for now because some devices are sending this very frequently + // TODO re-enable with dlna debug logging setting + //_logger.Debug("Creating event subscription for {0} with timeout of {1} to {2}", + // notificationType, + // timeout, + // callbackUrl); _subscriptions.TryAdd(id, new EventSubscription { @@ -61,7 +63,25 @@ namespace Emby.Dlna.Eventing TimeoutSeconds = timeout }); - return GetEventSubscriptionResponse(id, timeout); + return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout); + } + + private int? ParseTimeout(string header) + { + if (!string.IsNullOrEmpty(header)) + { + // Starts with SECOND- + header = header.Split('-').Last(); + + int val; + + if (int.TryParse(header, NumberStyles.Any, _usCulture, out val)) + { + return val; + } + } + + return null; } public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) @@ -73,22 +93,22 @@ namespace Emby.Dlna.Eventing return new EventSubscriptionResponse { - Content = "\r\n", + Content = string.Empty, ContentType = "text/plain" }; } private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, int timeoutSeconds) + private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds) { var response = new EventSubscriptionResponse { - Content = "\r\n", + Content = string.Empty, ContentType = "text/plain" }; response.Headers["SID"] = subscriptionId; - response.Headers["TIMEOUT"] = "SECOND-" + timeoutSeconds.ToString(_usCulture); + response.Headers["TIMEOUT"] = string.IsNullOrWhiteSpace(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString; return response; } diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs index 3eb2bc1d5c..d31dc155e9 100644 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs @@ -56,8 +56,9 @@ namespace Emby.Dlna.PlayTo if (profile.Container.Length > 0) { // Check container type - var mediaContainer = Path.GetExtension(mediaPath); - if (!profile.GetContainers().Any(i => string.Equals("." + i.TrimStart('.'), mediaContainer, StringComparison.OrdinalIgnoreCase))) + var mediaContainer = (Path.GetExtension(mediaPath) ?? string.Empty).TrimStart('.'); + + if (!profile.SupportsContainer(mediaContainer)) { return false; } diff --git a/Emby.Dlna/Profiles/LgTvProfile.cs b/Emby.Dlna/Profiles/LgTvProfile.cs index 71f684ec48..f81fb1ee79 100644 --- a/Emby.Dlna/Profiles/LgTvProfile.cs +++ b/Emby.Dlna/Profiles/LgTvProfile.cs @@ -53,14 +53,7 @@ namespace Emby.Dlna.Profiles { new DirectPlayProfile { - Container = "ts", - VideoCodec = "h264", - AudioCodec = "aac,ac3,mp3,dca,dts", - Type = DlnaProfileType.Video - }, - new DirectPlayProfile - { - Container = "mkv", + Container = "ts,mpegts,avi,mkv", VideoCodec = "h264", AudioCodec = "aac,ac3,mp3,dca,dts", Type = DlnaProfileType.Video diff --git a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml index a61fefcc8b..92e02799ed 100644 --- a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml +++ b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml @@ -35,8 +35,7 @@ false - - + diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs index 574d749588..ddc37da095 100644 --- a/Emby.Dlna/Service/BaseService.cs +++ b/Emby.Dlna/Service/BaseService.cs @@ -24,14 +24,14 @@ namespace Emby.Dlna.Service return EventManager.CancelEventSubscription(subscriptionId); } - public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, int? timeoutSeconds) + public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string timeoutString) { - return EventManager.RenewEventSubscription(subscriptionId, timeoutSeconds); + return EventManager.RenewEventSubscription(subscriptionId, timeoutString); } - public EventSubscriptionResponse CreateEventSubscription(string notificationType, int? timeoutSeconds, string callbackUrl) + public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl) { - return EventManager.CreateEventSubscription(notificationType, timeoutSeconds, callbackUrl); + return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl); } } } diff --git a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs index 98011ddd48..7be4101c80 100644 --- a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs +++ b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs @@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Channels return Task.FromResult>(new List()); } - public Task> OpenMediaSource(string openToken, CancellationToken cancellationToken) + public Task> OpenMediaSource(string openToken, bool allowLiveStreamProbe, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index df8d1ac451..f43e45441c 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -17,7 +17,7 @@ using MediaBrowser.Model.Tasks; namespace Emby.Server.Implementations.Data { - public class CleanDatabaseScheduledTask : IScheduledTask + public class CleanDatabaseScheduledTask : ILibraryPostScanTask { private readonly ILibraryManager _libraryManager; private readonly IItemRepository _itemRepo; @@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Data get { return "Library"; } } - public async Task Execute(CancellationToken cancellationToken, IProgress progress) + public async Task Run(IProgress progress, CancellationToken cancellationToken) { // Ensure these objects are lazy loaded. // Without this there is a deadlock that will need to be investigated diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 5486f404f3..1b0cbb936a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -307,7 +307,7 @@ - ..\packages\SQLitePCLRaw.core.1.1.6\lib\net45\SQLitePCLRaw.core.dll + ..\packages\SQLitePCLRaw.core.1.1.7\lib\net45\SQLitePCLRaw.core.dll True @@ -418,6 +418,9 @@ + + +