From d56fa09ccc5e1a5f9440645330ce337273fa3bd7 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 22 Jul 2014 12:36:34 -0400 Subject: [PATCH] stub out mock sync provider --- MediaBrowser.Api/Sync/SyncService.cs | 21 ++--- MediaBrowser.Controller/Sync/ISyncManager.cs | 20 ++--- .../Channels/DlnaChannelFactory.cs | 12 ++- MediaBrowser.Dlna/PlayTo/Device.cs | 14 ++-- MediaBrowser.Dlna/PlayTo/PlayToManager.cs | 1 - .../MediaBrowser.Model.Portable.csproj | 3 - .../MediaBrowser.Model.net35.csproj | 3 - MediaBrowser.Model/Dto/BaseItemDto.cs | 2 + MediaBrowser.Model/MediaBrowser.Model.csproj | 1 - MediaBrowser.Model/Querying/ItemFields.cs | 5 ++ MediaBrowser.Model/Sync/SyncJob.cs | 5 ++ MediaBrowser.Model/Sync/SyncJobRequest.cs | 12 ++- .../Sync/SyncScheduleRequest.cs | 23 ------ .../Dto/DtoService.cs | 10 ++- .../HttpServer/HttpListenerHost.cs | 2 +- ...MediaBrowser.Server.Implementations.csproj | 1 + .../Sync/AppSyncProvider.cs | 4 +- .../Sync/CloudSyncProvider.cs | 4 +- .../Sync/MockSyncProvider.cs | 33 ++++++++ .../Sync/SyncManager.cs | 77 ++++++++++++++++--- .../ApplicationHost.cs | 8 +- .../Savers/AlbumNfoSaver.cs | 33 +++----- 22 files changed, 193 insertions(+), 101 deletions(-) delete mode 100644 MediaBrowser.Model/Sync/SyncScheduleRequest.cs create mode 100644 MediaBrowser.Server.Implementations/Sync/MockSyncProvider.cs diff --git a/MediaBrowser.Api/Sync/SyncService.cs b/MediaBrowser.Api/Sync/SyncService.cs index 57b5fa3f90..929d0a4639 100644 --- a/MediaBrowser.Api/Sync/SyncService.cs +++ b/MediaBrowser.Api/Sync/SyncService.cs @@ -3,6 +3,7 @@ using MediaBrowser.Controller.Sync; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Sync; using ServiceStack; +using System.Collections.Generic; using System.Threading.Tasks; namespace MediaBrowser.Api.Sync @@ -50,9 +51,11 @@ namespace MediaBrowser.Api.Sync { } - [Route("/Sync/Schedules", "POST", Summary = "Gets sync schedules.")] - public class CreateSyncSchedule : SyncScheduleRequest + [Route("/Sync/Targets", "GET", Summary = "Gets a list of available sync targets.")] + public class GetSyncTarget : IReturn> { + [ApiMember(Name = "UserId", Description = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string UserId { get; set; } } [Authenticated] @@ -65,6 +68,13 @@ namespace MediaBrowser.Api.Sync _syncManager = syncManager; } + public object Get(GetSyncTarget request) + { + var result = _syncManager.GetSyncTargets(request.UserId); + + return ToOptimizedResult(result); + } + public object Get(GetSyncJobs request) { var result = _syncManager.GetJobs(new SyncJobQuery @@ -119,12 +129,5 @@ namespace MediaBrowser.Api.Sync Task.WaitAll(task); } - - public void Post(CreateSyncSchedule request) - { - var task = _syncManager.CreateSchedule(request); - - Task.WaitAll(task); - } } } diff --git a/MediaBrowser.Controller/Sync/ISyncManager.cs b/MediaBrowser.Controller/Sync/ISyncManager.cs index ef4b84f273..714e71a1f8 100644 --- a/MediaBrowser.Controller/Sync/ISyncManager.cs +++ b/MediaBrowser.Controller/Sync/ISyncManager.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Querying; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Querying; using MediaBrowser.Model.Sync; using System.Collections.Generic; using System.Threading.Tasks; @@ -14,13 +15,6 @@ namespace MediaBrowser.Controller.Sync /// Task. Task> CreateJob(SyncJobRequest request); - /// - /// Creates the schedule. - /// - /// The request. - /// Task. - Task CreateSchedule(SyncScheduleRequest request); - /// /// Gets the jobs. /// @@ -69,7 +63,13 @@ namespace MediaBrowser.Controller.Sync /// /// Gets the synchronize targets. /// - /// IEnumerable<SyncTarget>. - IEnumerable GetSyncTargets(); + IEnumerable GetSyncTargets(string userId); + + /// + /// Supportses the synchronize. + /// + /// The item. + /// true if XXXX, false otherwise. + bool SupportsSync(BaseItem item); } } diff --git a/MediaBrowser.Dlna/Channels/DlnaChannelFactory.cs b/MediaBrowser.Dlna/Channels/DlnaChannelFactory.cs index 414bec7152..230b5e145e 100644 --- a/MediaBrowser.Dlna/Channels/DlnaChannelFactory.cs +++ b/MediaBrowser.Dlna/Channels/DlnaChannelFactory.cs @@ -86,9 +86,17 @@ namespace MediaBrowser.Dlna.Channels return; } - var device = await Device.CreateuPnpDeviceAsync(new Uri(location), _httpClient, _config, _logger).ConfigureAwait(false); + var device = await Device.CreateuPnpDeviceAsync(new Uri(location), _httpClient, _config, _logger) + .ConfigureAwait(false); - _servers.Add(device); + if (!_servers.Any(i => string.Equals(i.Properties.UUID, device.Properties.UUID, StringComparison.OrdinalIgnoreCase))) + { + _servers.Add(device); + } + } + catch (Exception ex) + { + } finally { diff --git a/MediaBrowser.Dlna/PlayTo/Device.cs b/MediaBrowser.Dlna/PlayTo/Device.cs index 272e34a789..0d7c450d61 100644 --- a/MediaBrowser.Dlna/PlayTo/Device.cs +++ b/MediaBrowser.Dlna/PlayTo/Device.cs @@ -757,13 +757,17 @@ namespace MediaBrowser.Dlna.PlayTo var deviceProperties = new DeviceInfo(); + var friendlyNames = new List(); + var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault(); - if (name != null) - deviceProperties.Name = name.Value; + if (name != null && !string.IsNullOrWhiteSpace(name.Value)) + friendlyNames.Add(name.Value); + + var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault(); + if (room != null && !string.IsNullOrWhiteSpace(room.Value)) + friendlyNames.Add(room.Value); - var name2 = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault(); - if (name2 != null) - deviceProperties.Name = name2.Value; + deviceProperties.Name = string.Join(" ", friendlyNames.ToArray()); var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault(); if (model != null) diff --git a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs index cea397cae3..a5b816919f 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs @@ -7,7 +7,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Session; using MediaBrowser.Dlna.Ssdp; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Session; using System; diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index 17463941c6..a892f8bf16 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -866,9 +866,6 @@ Sync\SyncScheduleQuery.cs - - Sync\SyncScheduleRequest.cs - Sync\SyncTarget.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index 9dafe0cef0..ab2a99109e 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -823,9 +823,6 @@ Sync\SyncScheduleQuery.cs - - Sync\SyncScheduleRequest.cs - Sync\SyncTarget.cs diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 7f7a4b8f08..d138ddf9bc 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -53,6 +53,8 @@ namespace MediaBrowser.Model.Dto public bool IsUnidentified { get; set; } public int? AnimeSeriesIndex { get; set; } + + public bool? SupportsSync { get; set; } /// /// Gets or sets the DVD season number. diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 3c265ff917..6cdd2b8f5e 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -303,7 +303,6 @@ - diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 790638446e..9ceca311c3 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -156,6 +156,11 @@ namespace MediaBrowser.Model.Querying /// Studios, + /// + /// The synchronize information + /// + SyncInfo, + /// /// The taglines of the item /// diff --git a/MediaBrowser.Model/Sync/SyncJob.cs b/MediaBrowser.Model/Sync/SyncJob.cs index 34809834cf..74dd79497c 100644 --- a/MediaBrowser.Model/Sync/SyncJob.cs +++ b/MediaBrowser.Model/Sync/SyncJob.cs @@ -43,5 +43,10 @@ namespace MediaBrowser.Model.Sync /// /// The transcoded path. public string TranscodedPath { get; set; } + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } } } diff --git a/MediaBrowser.Model/Sync/SyncJobRequest.cs b/MediaBrowser.Model/Sync/SyncJobRequest.cs index 42b744db66..cd833068ee 100644 --- a/MediaBrowser.Model/Sync/SyncJobRequest.cs +++ b/MediaBrowser.Model/Sync/SyncJobRequest.cs @@ -10,19 +10,25 @@ namespace MediaBrowser.Model.Sync /// The device identifier. public List TargetIds { get; set; } /// - /// Gets or sets the item identifier. + /// Gets or sets the item ids. /// - /// The item identifier. - public string ItemId { get; set; } + /// The item ids. + public List ItemIds { get; set; } /// /// Gets or sets the quality. /// /// The quality. public SyncQuality Quality { get; set; } + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } public SyncJobRequest() { TargetIds = new List(); + ItemIds = new List(); } } } diff --git a/MediaBrowser.Model/Sync/SyncScheduleRequest.cs b/MediaBrowser.Model/Sync/SyncScheduleRequest.cs deleted file mode 100644 index 076548f57c..0000000000 --- a/MediaBrowser.Model/Sync/SyncScheduleRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace MediaBrowser.Model.Sync -{ - public class SyncScheduleRequest - { - /// - /// Gets or sets the device identifier. - /// - /// The device identifier. - public List TargetIds { get; set; } - /// - /// Gets or sets the quality. - /// - /// The quality. - public SyncQuality Quality { get; set; } - - public SyncScheduleRequest() - { - TargetIds = new List(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index f01d973d64..c4499fac01 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sync; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -38,8 +39,9 @@ namespace MediaBrowser.Server.Implementations.Dto private readonly IProviderManager _providerManager; private readonly Func _channelManagerFactory; + private readonly ISyncManager _syncManager; - public DtoService(ILogger logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IImageProcessor imageProcessor, IServerConfigurationManager config, IFileSystem fileSystem, IProviderManager providerManager, Func channelManagerFactory) + public DtoService(ILogger logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IImageProcessor imageProcessor, IServerConfigurationManager config, IFileSystem fileSystem, IProviderManager providerManager, Func channelManagerFactory, ISyncManager syncManager) { _logger = logger; _libraryManager = libraryManager; @@ -50,6 +52,7 @@ namespace MediaBrowser.Server.Implementations.Dto _fileSystem = fileSystem; _providerManager = providerManager; _channelManagerFactory = channelManagerFactory; + _syncManager = syncManager; } /// @@ -146,6 +149,11 @@ namespace MediaBrowser.Server.Implementations.Dto AttachBasicFields(dto, item, owner, fields); + if (fields.Contains(ItemFields.SyncInfo)) + { + dto.SupportsSync = _syncManager.SupportsSync(item); + } + if (fields.Contains(ItemFields.SoundtrackIds)) { var hasSoundtracks = item as IHasSoundtracks; diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 94715b4905..d0ea070563 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -158,7 +158,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First()); _listener = NativeWebSocket.IsSupported - ? _listener = new WebSocketSharpListener(_logger, _threadPoolManager) + ? _listener = new HttpListenerServer(_logger, _threadPoolManager) : _listener = new WebSocketSharpListener(_logger, _threadPoolManager); _listener.WebSocketHandler = WebSocketHandler; diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 38e82db4f3..9655771c66 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -272,6 +272,7 @@ + diff --git a/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs b/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs index 6dec2b7175..c3cd047b6b 100644 --- a/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs +++ b/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs @@ -10,12 +10,12 @@ namespace MediaBrowser.Server.Implementations.Sync { public IEnumerable GetSyncTargets() { - throw new NotImplementedException(); + return new List(); } public DeviceProfile GetDeviceProfile(SyncTarget target) { - throw new NotImplementedException(); + return new DeviceProfile(); } public string Name diff --git a/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs b/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs index ec3ac12397..fd12b1f8aa 100644 --- a/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs +++ b/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs @@ -19,12 +19,12 @@ namespace MediaBrowser.Server.Implementations.Sync public IEnumerable GetSyncTargets() { - throw new NotImplementedException(); + return new List(); } public DeviceProfile GetDeviceProfile(SyncTarget target) { - throw new NotImplementedException(); + return new DeviceProfile(); } public string Name diff --git a/MediaBrowser.Server.Implementations/Sync/MockSyncProvider.cs b/MediaBrowser.Server.Implementations/Sync/MockSyncProvider.cs new file mode 100644 index 0000000000..bc079ad809 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/MockSyncProvider.cs @@ -0,0 +1,33 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Sync; +using System.Collections.Generic; + +namespace MediaBrowser.Server.Implementations.Sync +{ + public class MockSyncProvider : ISyncProvider + { + public string Name + { + get { return "Dummy Sync"; } + } + + public IEnumerable GetSyncTargets() + { + return new List + { + new SyncTarget + { + Id = "mock".GetMD5().ToString("N"), + Name = "Mock Sync" + } + }; + } + + public DeviceProfile GetDeviceProfile(SyncTarget target) + { + return new DeviceProfile(); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs index 508fc188dd..50f1030f3c 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs @@ -1,4 +1,7 @@ -using MediaBrowser.Controller.Sync; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Sync; using System; @@ -10,7 +13,7 @@ namespace MediaBrowser.Server.Implementations.Sync { public class SyncManager : ISyncManager { - private ISyncProvider[] _providers = new ISyncProvider[]{}; + private ISyncProvider[] _providers = new ISyncProvider[] { }; public void AddParts(IEnumerable providers) { @@ -22,11 +25,6 @@ namespace MediaBrowser.Server.Implementations.Sync throw new NotImplementedException(); } - public Task CreateSchedule(SyncScheduleRequest request) - { - throw new NotImplementedException(); - } - public QueryResult GetJobs(SyncJobQuery query) { throw new NotImplementedException(); @@ -57,9 +55,70 @@ namespace MediaBrowser.Server.Implementations.Sync throw new NotImplementedException(); } - public IEnumerable GetSyncTargets() + public IEnumerable GetSyncTargets(string userId) + { + return _providers + .SelectMany(GetSyncTargets) + .OrderBy(i => i.Name); + } + + private IEnumerable GetSyncTargets(ISyncProvider provider) + { + var providerId = GetSyncProviderId(provider); + + return provider.GetSyncTargets().Select(i => new SyncTarget + { + Name = i.Name, + Id = providerId + "-" + i.Id + }); + } + + private ISyncProvider GetSyncProvider(SyncTarget target) + { + var providerId = target.Id.Split(new[] { '-' }, 2).First(); + + return _providers.First(i => string.Equals(providerId, GetSyncProviderId(i))); + } + + private string GetSyncProviderId(ISyncProvider provider) + { + return (provider.GetType().Name + provider.Name).GetMD5().ToString("N"); + } + + public bool SupportsSync(BaseItem item) { - return _providers.SelectMany(i => i.GetSyncTargets()); + if (item.LocationType == LocationType.Virtual) + { + return false; + } + + if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) || + string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + if (item.RunTimeTicks.HasValue) + { + var video = item as Video; + + if (video != null) + { + if (video.VideoType != VideoType.VideoFile) + { + return false; + } + + if (video.IsMultiPart) + { + return false; + } + } + + return true; + } + + return false; + } + + return false; } } } diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 2208bc2170..55843bbdbe 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -561,7 +561,10 @@ namespace MediaBrowser.ServerApplication ImageProcessor = new ImageProcessor(LogManager.GetLogger("ImageProcessor"), ServerConfigurationManager.ApplicationPaths, FileSystemManager, JsonSerializer, MediaEncoder); RegisterSingleInstance(ImageProcessor); - DtoService = new DtoService(Logger, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager); + SyncManager = new SyncManager(); + RegisterSingleInstance(SyncManager); + + DtoService = new DtoService(Logger, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager, SyncManager); RegisterSingleInstance(DtoService); SessionManager = new SessionManager(UserDataManager, ServerConfigurationManager, Logger, UserRepository, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, ItemRepository, JsonSerializer, this, HttpClient, AuthenticationRepository); @@ -614,9 +617,6 @@ namespace MediaBrowser.ServerApplication MediaEncoder, ChapterManager); RegisterSingleInstance(EncodingManager); - SyncManager = new SyncManager(); - RegisterSingleInstance(SyncManager); - var authContext = new AuthorizationContext(); RegisterSingleInstance(authContext); RegisterSingleInstance(new SessionContext(UserManager, authContext, SessionManager)); diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index 79b262fc31..f9a6fc059c 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -43,37 +43,25 @@ namespace MediaBrowser.XbmcMetadata.Savers { var album = (MusicAlbum)item; - var tracks = album.Tracks - .ToList(); - - var artists = tracks - .SelectMany(i => - { - var list = new List(); - - if (!string.IsNullOrEmpty(i.AlbumArtist)) - { - list.Add(i.AlbumArtist); - } - list.AddRange(i.Artists); - - return list; - }) - .Distinct(StringComparer.OrdinalIgnoreCase); - - foreach (var artist in artists) + foreach (var artist in album.Artists) { writer.WriteElementString("artist", artist); } - AddTracks(tracks, writer); + foreach (var artist in album.AlbumArtists) + { + writer.WriteElementString("albumartist", artist); + } + + AddTracks(album.Tracks, writer); } private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); private void AddTracks(IEnumerable