diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f22944a8b6..d69e6330bb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -30,6 +30,7 @@ - [Khinenw](https://github.com/HelloWorld017) - [fhriley](https://github.com/fhriley) - [nevado](https://github.com/nevado) + - [mark-monteiro](https://github.com/mark-monteiro) # Emby Contributors diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ae979682e6..af831ff78d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -838,16 +838,14 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(ChapterManager); MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( - LoggerFactory, - JsonSerializer, - StartupOptions.FFmpegPath, + LoggerFactory.CreateLogger(), ServerConfigurationManager, FileSystemManager, - () => SubtitleEncoder, - () => MediaSourceManager, ProcessFactory, - 5000, - LocalizationManager); + LocalizationManager, + () => SubtitleEncoder, + _configuration, + StartupOptions.FFmpegPath); serviceCollection.AddSingleton(MediaEncoder); EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager); @@ -864,10 +862,19 @@ namespace Emby.Server.Implementations AuthService = new AuthService(LoggerFactory.CreateLogger(), authContext, ServerConfigurationManager, SessionManager, NetworkManager); serviceCollection.AddSingleton(AuthService); - SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LoggerFactory, ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory); + SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder( + LibraryManager, + LoggerFactory.CreateLogger(), + ApplicationPaths, + FileSystemManager, + MediaEncoder, + HttpClient, + MediaSourceManager, + ProcessFactory); serviceCollection.AddSingleton(SubtitleEncoder); serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager)); + serviceCollection.AddSingleton(); _displayPreferencesRepository.Initialize(); diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 62408ee703..2ea7ff6e91 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -1,13 +1,16 @@ using System.Collections.Generic; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations { public static class ConfigurationOptions { - public static readonly Dictionary Configuration = new Dictionary + public static Dictionary Configuration => new Dictionary { { "HttpListenerHost:DefaultRedirectPath", "web/index.html" }, - { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" } + { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" }, + { FfmpegProbeSizeKey, "1G" }, + { FfmpegAnalyzeDurationKey, "200M" } }; } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 5ca5087765..54bf5df832 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -15,12 +15,12 @@ - + @@ -29,7 +29,6 @@ - diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 5d4f17861e..f1fb35d9ac 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -227,24 +227,21 @@ namespace Emby.Server.Implementations.Library { var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100; - // Don't track in very beginning if (pctIn < _config.Configuration.MinResumePct) { + // ignore progress during the beginning positionTicks = 0; } - - // If we're at the end, assume completed else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks) { + // mark as completed close to the end positionTicks = 0; data.Played = playedToCompletion = true; } - else { // Enforce MinResumeDuration var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds; - if (durationSeconds < _config.Configuration.MinResumeDurationSeconds) { positionTicks = 0; @@ -264,6 +261,7 @@ namespace Emby.Server.Implementations.Library positionTicks = 0; data.Played = false; } + if (!item.SupportsPositionTicksResume) { positionTicks = 0; diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 88693f22a4..1f38de2d86 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -7,8 +7,8 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Emby.XmlTv.Classes; -using Emby.XmlTv.Entities; +using Jellyfin.XmlTv; +using Jellyfin.XmlTv.Entities; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 2ecf4e1847..ee7db1413d 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -304,9 +304,12 @@ namespace Emby.Server.Implementations.LiveTv } private ILiveTvService GetService(string name) - { - return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); - } + => Array.Find(_services, x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)) + ?? throw new KeyNotFoundException( + string.Format( + CultureInfo.InvariantCulture, + "No service with the name '{0}' can be found.", + name)); private static void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo) { diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 74406a0641..9961b09841 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -1,11 +1,11 @@ { "Albums": "Àlbums", - "AppDeviceValues": "App: {0}, Dispositiu: {1}", - "Application": "Application", + "AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}", + "Application": "Aplicació", "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", "Books": "Llibres", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "CameraImageUploadedFrom": "Una nova imatge de càmera ha sigut pujada des de {0}", "Channels": "Canals", "ChapterNameValue": "Episodi {0}", "Collections": "Col·leccions", @@ -15,8 +15,8 @@ "Favorites": "Preferits", "Folders": "Directoris", "Genres": "Gèneres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", + "HeaderAlbumArtists": "Artistes dels Àlbums", + "HeaderCameraUploads": "Pujades de Càmera", "HeaderContinueWatching": "Continua Veient", "HeaderFavoriteAlbums": "Àlbums Preferits", "HeaderFavoriteArtists": "Artistes Preferits", @@ -27,71 +27,71 @@ "HeaderNextUp": "A continuació", "HeaderRecordingGroups": "Grups d'Enregistrament", "HomeVideos": "Vídeos domèstics", - "Inherit": "Heretat", - "ItemAddedWithName": "{0} afegit a la biblioteca", - "ItemRemovedWithName": "{0} eliminat de la biblioteca", + "Inherit": "Hereta", + "ItemAddedWithName": "{0} ha estat afegit a la biblioteca", + "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca", "LabelIpAddressValue": "Adreça IP: {0}", - "LabelRunningTimeValue": "Temps en marxa: {0}", + "LabelRunningTimeValue": "Temps en funcionament: {0}", "Latest": "Darreres", - "MessageApplicationUpdated": "El Servidor d'Jellyfin ha estat actualitzat", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "La secció de configuració {0} ha estat actualitzada", + "MessageApplicationUpdated": "El Servidor de Jellyfin ha estat actualitzat", + "MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada", "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor", "MixedContent": "Contingut mesclat", "Movies": "Pel·lícules", "Music": "Música", "MusicVideos": "Vídeos musicals", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "Instalació de {0} fallida", "NameSeasonNumber": "Temporada {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "NameSeasonUnknown": "Temporada Desconeguda", + "NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.", "NotificationOptionApplicationUpdateAvailable": "Actualització d'aplicació disponible", "NotificationOptionApplicationUpdateInstalled": "Actualització d'aplicació instal·lada", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Un component ha fallat", - "NotificationOptionPluginInstalled": "Complement instal·lat", - "NotificationOptionPluginUninstalled": "Complement desinstal·lat", - "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", + "NotificationOptionAudioPlayback": "Reproducció d'audio iniciada", + "NotificationOptionAudioPlaybackStopped": "Reproducció d'audio aturada", + "NotificationOptionCameraImageUploaded": "Imatge de càmera pujada", + "NotificationOptionInstallationFailed": "Instalació fallida", + "NotificationOptionNewLibraryContent": "Nou contingut afegit", + "NotificationOptionPluginError": "Un connector ha fallat", + "NotificationOptionPluginInstalled": "Connector instal·lat", + "NotificationOptionPluginUninstalled": "Connector desinstal·lat", + "NotificationOptionPluginUpdateInstalled": "Actualització de connector instal·lada", + "NotificationOptionServerRestartRequired": "Reinici del servidor requerit", + "NotificationOptionTaskFailed": "Tasca programada fallida", + "NotificationOptionUserLockedOut": "Usuari tancat", + "NotificationOptionVideoPlayback": "Reproducció de video iniciada", + "NotificationOptionVideoPlaybackStopped": "Reproducció de video aturada", "Photos": "Fotos", "Playlists": "Llistes de reproducció", - "Plugin": "Plugin", + "Plugin": "Connector", "PluginInstalledWithName": "{0} ha estat instal·lat", "PluginUninstalledWithName": "{0} ha estat desinstal·lat", "PluginUpdatedWithName": "{0} ha estat actualitzat", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", "ScheduledTaskStartedWithName": "{0} iniciat", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Espectacles", + "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat", + "Shows": "Programes", "Songs": "Cançons", "StartupEmbyServerIsLoading": "El Servidor d'Jellyfin està carregant. Si et plau, prova de nou en breus.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}", "SubtitlesDownloadedForItem": "Subtítols descarregats per a {0}", - "Sync": "Sync", + "Sync": "Sincronitzar", "System": "System", "TvShows": "Espectacles de TV", "User": "User", "UserCreatedWithName": "S'ha creat l'usuari {0}", "UserDeletedWithName": "L'usuari {0} ha estat eliminat", "UserDownloadingItemWithValues": "{0} està descarregant {1}", - "UserLockedOutWithName": "User {0} has been locked out", + "UserLockedOutWithName": "L'usuari {0} ha sigut tancat", "UserOfflineFromDevice": "{0} s'ha desconnectat de {1}", "UserOnlineFromDevice": "{0} està connectat des de {1}", "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", + "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per {0}", "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}", "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva llibreria", "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versió {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index 982232afd6..c3b5211b84 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -7,11 +7,11 @@ "HeaderRecordingGroups": "Upptökuhópar", "HeaderNextUp": "Næst á dagskrá", "HeaderLiveTV": "Sjónvarp í beinni útsendingu", - "HeaderFavoriteSongs": "Uppáhalds lög", - "HeaderFavoriteShows": "Uppáhalds sjónvarpsþættir", - "HeaderFavoriteEpisodes": "Uppáhalds þættir", - "HeaderFavoriteArtists": "Uppáhalds listamenn", - "HeaderFavoriteAlbums": "Uppáhalds plötur", + "HeaderFavoriteSongs": "Uppáhalds Lög", + "HeaderFavoriteShows": "Uppáhalds Sjónvarpsþættir", + "HeaderFavoriteEpisodes": "Uppáhalds Þættir", + "HeaderFavoriteArtists": "Uppáhalds Listamenn", + "HeaderFavoriteAlbums": "Uppáhalds Plötur", "HeaderContinueWatching": "Halda áfram að horfa", "HeaderCameraUploads": "Myndavéla upphal", "HeaderAlbumArtists": "Höfundur plötu", @@ -30,5 +30,49 @@ "Artists": "Listamaður", "Application": "Forrit", "AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}", - "Albums": "Plötur" + "Albums": "Plötur", + "Plugin": "Viðbót", + "Photos": "Myndir", + "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð", + "NotificationOptionVideoPlayback": "Myndbandafspilun hafin", + "NotificationOptionUserLockedOut": "Notandi læstur úti", + "NotificationOptionServerRestartRequired": "Endurræsing miðlara nauðsynileg", + "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett", + "NotificationOptionPluginUninstalled": "Viðbót fjarlægð", + "NotificationOptionPluginInstalled": "Viðbót settur upp", + "NotificationOptionPluginError": "Bilun í viðbót", + "NotificationOptionInstallationFailed": "Uppsetning tókst ekki", + "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp", + "NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð", + "NotificationOptionAudioPlayback": "Hljóðafspilun hafin", + "NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett", + "NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði", + "NameSeasonUnknown": "Sería óþekkt", + "NameSeasonNumber": "Sería {0}", + "MixedContent": "Blandað efni", + "MessageServerConfigurationUpdated": "Stillingar miðlarans hefur verið uppfærð", + "MessageApplicationUpdatedTo": "Jellyfin Server hefur verið uppfærður í {0}", + "MessageApplicationUpdated": "Jellyfin Server hefur verið uppfærður", + "Latest": "Nýjasta", + "LabelRunningTimeValue": "Keyrslutími kerfis: {0}", + "User": "Notandi", + "System": "Kerfi", + "NotificationOptionNewLibraryContent": "Nýju efni bætt við", + "NewVersionIsAvailable": "Ný útgáfa af Jellyfin Server er fáanleg til niðurhals.", + "NameInstallFailed": "{0} uppsetning mistókst", + "MusicVideos": "Tónlistarmyndbönd", + "Music": "Tónlist", + "Movies": "Kvikmyndir", + "UserDeletedWithName": "Notanda {0} hefur verið eytt", + "UserCreatedWithName": "Notandi {0} hefur verið stofnaður", + "TvShows": "Þættir", + "Sync": "Samstilla", + "Songs": "Lög", + "ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa", + "ScheduledTaskStartedWithName": "{0} hafin", + "ScheduledTaskFailedWithName": "{0} mistókst", + "PluginUpdatedWithName": "{0} var uppfært", + "PluginUninstalledWithName": "{0} var fjarlægt", + "PluginInstalledWithName": "{0} var sett upp", + "NotificationOptionTaskFailed": "Tímasett verkefni mistókst" } diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index 3d2350c07a..0320a0a1b7 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -5,13 +5,13 @@ "Artists": "아티스트", "AuthenticationSucceededWithUserName": "{0}이 성공적으로 인증됨", "Books": "도서", - "CameraImageUploadedFrom": "{0}에서 새로운 카메라 이미지가 업로드되었습니다", + "CameraImageUploadedFrom": "{0}에서 새로운 카메라 이미지가 업로드됨", "Channels": "채널", "ChapterNameValue": "챕터 {0}", "Collections": "컬렉션", - "DeviceOfflineWithName": "{0} 연결 끊김", - "DeviceOnlineWithName": "{0} 연결됨", - "FailedLoginAttemptWithUserName": "{0} 로그인 실패", + "DeviceOfflineWithName": "{0}의 연결 끊김", + "DeviceOnlineWithName": "{0}이 연결됨", + "FailedLoginAttemptWithUserName": "{0}에서 로그인 실패", "Favorites": "즐겨찾기", "Folders": "폴더", "Genres": "장르", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 637e514ed6..4423b7f988 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -5,10 +5,10 @@ "Artists": "Artiesten", "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd", "Books": "Boeken", - "CameraImageUploadedFrom": "Er is een nieuwe foto toegevoegd via {0}", + "CameraImageUploadedFrom": "Er is een nieuwe foto toegevoegd van {0}", "Channels": "Kanalen", "ChapterNameValue": "Hoofdstuk {0}", - "Collections": "Collecties", + "Collections": "Verzamelingen", "DeviceOfflineWithName": "{0} heeft de verbinding verbroken", "DeviceOnlineWithName": "{0} is verbonden", "FailedLoginAttemptWithUserName": "Mislukte aanmeld poging van {0}", @@ -58,7 +58,7 @@ "NotificationOptionPluginUpdateInstalled": "Plug-in-update geïnstalleerd", "NotificationOptionServerRestartRequired": "Server herstart nodig", "NotificationOptionTaskFailed": "Geplande taak mislukt", - "NotificationOptionUserLockedOut": "Gebruikersaccount vergrendeld", + "NotificationOptionUserLockedOut": "Gebruiker is vergrendeld", "NotificationOptionVideoPlayback": "Video gestart", "NotificationOptionVideoPlaybackStopped": "Video gestopt", "Photos": "Foto's", diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index fcc724a7df..41a389e3b1 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -24,7 +24,7 @@ "HeaderFavoriteShows": "Séries Favoritas", "HeaderFavoriteSongs": "Músicas Favoritas", "HeaderLiveTV": "TV ao Vivo", - "HeaderNextUp": "Próximos", + "HeaderNextUp": "A Seguir", "HeaderRecordingGroups": "Grupos de Gravação", "HomeVideos": "Vídeos caseiros", "Inherit": "Herdar", diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index b87ca3a114..b1d513dd4f 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -667,12 +667,9 @@ namespace Emby.Server.Implementations.Session data.PlayCount++; data.LastPlayedDate = DateTime.UtcNow; - if (item.SupportsPlayedStatus) + if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume) { - if (!(item is Video)) - { - data.Played = true; - } + data.Played = true; } else { @@ -769,7 +766,6 @@ namespace Emby.Server.Implementations.Session { _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None); } - } private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data) @@ -1393,6 +1389,12 @@ namespace Emby.Server.Implementations.Session } } + if (user == null) + { + AuthenticationFailed?.Invoke(this, new GenericEventArgs(request)); + throw new SecurityException("Invalid user or password entered."); + } + if (enforcePassword) { user = await _userManager.AuthenticateUser( @@ -1403,13 +1405,6 @@ namespace Emby.Server.Implementations.Session true).ConfigureAwait(false); } - if (user == null) - { - AuthenticationFailed?.Invoke(this, new GenericEventArgs(request)); - - throw new SecurityException("Invalid user or password entered."); - } - var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); var session = LogSessionActivity( diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 09a5a0dca8..2705e0628b 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -141,8 +141,7 @@ namespace Emby.Server.Implementations.Updates if (guid != Guid.Empty) { - var strGuid = guid.ToString("N", CultureInfo.InvariantCulture); - availablePackages = availablePackages.Where(x => x.guid.Equals(strGuid, StringComparison.OrdinalIgnoreCase)); + availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid); } return availablePackages; diff --git a/Emby.XmlTv/.gitattributes b/Emby.XmlTv/.gitattributes deleted file mode 100644 index 1ff0c42304..0000000000 --- a/Emby.XmlTv/.gitattributes +++ /dev/null @@ -1,63 +0,0 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain diff --git a/Emby.XmlTv/.gitignore b/Emby.XmlTv/.gitignore deleted file mode 100644 index b06e864a31..0000000000 --- a/Emby.XmlTv/.gitignore +++ /dev/null @@ -1,212 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -## TODO: Comment the next line if you want to checkin your -## web deploy settings but do note that will include unencrypted -## passwords -#*.pubxml - -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Windows Azure Build Output -csx/ -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# LightSwitch generated files -GeneratedArtifacts/ -_Pvt_Extensions/ -ModelManifest.xml diff --git a/Emby.XmlTv/Emby.XmlTv.Console/App.config b/Emby.XmlTv/Emby.XmlTv.Console/App.config deleted file mode 100644 index 2d2a12d81b..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Console/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Emby.XmlTv/Emby.XmlTv.Console/Classes/EntityExtensions.cs b/Emby.XmlTv/Emby.XmlTv.Console/Classes/EntityExtensions.cs deleted file mode 100644 index 96e508f122..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Console/Classes/EntityExtensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Linq; -using System.Text; - -using Emby.XmlTv.Entities; - -namespace Emby.XmlTv.Console.Classes -{ - public static class EntityExtensions - { - public static string GetHeader(this string text) - { - var channelHeaderString = " " + text; - - var builder = new StringBuilder(); - builder.AppendLine("".PadRight(5 + channelHeaderString.Length + 5, Char.Parse("*"))); - builder.AppendLine("".PadRight(5, Char.Parse("*")) + channelHeaderString + "".PadRight(5, Char.Parse("*"))); - builder.AppendLine("".PadRight(5 + channelHeaderString.Length + 5, Char.Parse("*"))); - - return builder.ToString(); - } - - public static string GetChannelDetail(this XmlTvChannel channel) - { - var builder = new StringBuilder(); - builder.AppendFormat("Id: {0}\r\n", channel.Id); - builder.AppendFormat("Display-Name: {0}\r\n", channel.DisplayName); - builder.AppendFormat("Url: {0}\r\n", channel.Url); - builder.AppendFormat("Icon: {0}\r\n", channel.Icon != null ? channel.Icon.ToString() : string.Empty); - builder.AppendLine("-------------------------------------------------------"); - - return builder.ToString(); - } - - public static string GetProgrammeDetail(this XmlTvProgram programme, XmlTvChannel channel) - { - var builder = new StringBuilder(); - builder.AppendFormat("Channel: {0} - {1}\r\n", channel.Id, channel.DisplayName); - builder.AppendFormat("Start Date: {0:G}\r\n", programme.StartDate); - builder.AppendFormat("End Date: {0:G}\r\n", programme.EndDate); - builder.AppendFormat("Name: {0}\r\n", programme.Title); - builder.AppendFormat("Episode Detail: {0}\r\n", programme.Episode); - builder.AppendFormat("Episode Title: {0}\r\n", programme.Episode.Title); - builder.AppendFormat("Description: {0}\r\n", programme.Description); - builder.AppendFormat("Categories: {0}\r\n", string.Join(", ", programme.Categories)); - builder.AppendFormat("Countries: {0}\r\n", string.Join(", ", programme.Countries)); - builder.AppendFormat("Credits: {0}\r\n", string.Join(", ", programme.Credits)); - builder.AppendFormat("Rating: {0}\r\n", programme.Rating); - builder.AppendFormat("Star Rating: {0}\r\n", programme.StarRating.HasValue ? programme.StarRating.Value.ToString() : string.Empty); - builder.AppendFormat("Previously Shown: {0:G}\r\n", programme.PreviouslyShown); - builder.AppendFormat("Copyright Date: {0:G}\r\n", programme.CopyrightDate); - builder.AppendFormat("Is Repeat: {0}\r\n", programme.IsPreviouslyShown); - builder.AppendFormat("Icon: {0}\r\n", programme.Icon != null ? programme.Icon.ToString() : string.Empty); - builder.AppendLine("-------------------------------------------------------"); - return builder.ToString(); - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv.Console/Emby.XmlTv.Console.csproj b/Emby.XmlTv/Emby.XmlTv.Console/Emby.XmlTv.Console.csproj deleted file mode 100644 index c10b28e824..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Console/Emby.XmlTv.Console.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - {0d023565-5942-4d79-9098-a1b4b6665a40} - Emby.XmlTv - - - - netcoreapp2.1 - false - - - diff --git a/Emby.XmlTv/Emby.XmlTv.Console/Program.cs b/Emby.XmlTv/Emby.XmlTv.Console/Program.cs deleted file mode 100644 index 3e0c7d1254..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Console/Program.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Emby.XmlTv.Classes; -using Emby.XmlTv.Console.Classes; -using Emby.XmlTv.Entities; - -namespace Emby.XmlTv.Console -{ - public class Program - { - static void Main(string[] args) - { - var filename = @"C:\Temp\QLD.GoldCoast.xml"; - - if (args.Length == 1 && File.Exists(args[0])) - { - filename = args[0]; - } - - var timer = Stopwatch.StartNew(); - System.Console.WriteLine("Running XMLTv Parsing"); - - var resultsFile = String.Format("C:\\Temp\\{0}_Results_{1:HHmmss}.txt", - Path.GetFileNameWithoutExtension(filename), - DateTimeOffset.UtcNow); - - System.Console.Write("Enter the language required: "); - var lang = System.Console.ReadLine(); - - ReadSourceXmlTvFile(filename, resultsFile, lang).Wait(); - - System.Console.WriteLine("Completed in {0:g} - press any key to open the file...", timer.Elapsed); - System.Console.ReadKey(); - - Process.Start(resultsFile); - } - - public static async Task ReadSourceXmlTvFile(string filename, string resultsFile, string lang) - { - System.Console.WriteLine("Writing to file: {0}", resultsFile); - - using (var resultsFileStream = new StreamWriter(resultsFile) { AutoFlush = true }) - { - var reader = new XmlTvReader(filename, lang); - await ReadOutChannels(reader, resultsFileStream); - - resultsFileStream.Close(); - } - } - - public static async Task ReadOutChannels(XmlTvReader reader, StreamWriter resultsFileStream) - { - var channels = reader.GetChannels().Distinct().ToList(); - - resultsFileStream.Write(EntityExtensions.GetHeader("Channels")); - - foreach (var channel in channels) - { - System.Console.WriteLine("Retrieved Channel: {0} - {1}", channel.Id, channel.DisplayName); - resultsFileStream.Write(channel.GetChannelDetail()); - } - - var totalProgrammeCount = 0; - - resultsFileStream.Write("\r\n"); - foreach (var channel in channels) - { - System.Console.WriteLine("Processing Channel: {0}", channel.DisplayName); - - resultsFileStream.Write(EntityExtensions.GetHeader("Programs for " + channel.DisplayName)); - var channelProgrammeCount = await ReadOutChannelProgrammes(reader, channel, resultsFileStream); - - totalProgrammeCount += channelProgrammeCount; - await resultsFileStream.WriteLineAsync(String.Format("Total Programmes for {1}: {0}", channelProgrammeCount, channel.DisplayName)); - } - - await resultsFileStream.WriteLineAsync(String.Format("Total Programmes: {0}", totalProgrammeCount)); - } - - private static async Task ReadOutChannelProgrammes(XmlTvReader reader, XmlTvChannel channel, StreamWriter resultsFileStream) - { - //var startDate = new DateTime(2015, 11, 28); - //var endDate = new DateTime(2015, 11, 29); - var startDate = DateTimeOffset.MinValue; - var endDate = DateTimeOffset.MaxValue; - - var count = 0; - - foreach (var programme in reader.GetProgrammes(channel.Id, startDate, endDate, new CancellationToken()).Distinct()) - { - count++; - await resultsFileStream.WriteLineAsync(programme.GetProgrammeDetail(channel)); - } - - return count; - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv.Console/Properties/AssemblyInfo.cs b/Emby.XmlTv/Emby.XmlTv.Console/Properties/AssemblyInfo.cs deleted file mode 100644 index ff59f890fd..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Console/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Emby.XmlTv.Console")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Emby.XmlTv.Console")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("add1d993-6d74-480a-b1fc-7fd9fd05a769")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Emby.XmlTv/Emby.XmlTv.Test/Emby.XmlTv.Test.csproj b/Emby.XmlTv/Emby.XmlTv.Test/Emby.XmlTv.Test.csproj deleted file mode 100644 index d7c4ad0b72..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Test/Emby.XmlTv.Test.csproj +++ /dev/null @@ -1,124 +0,0 @@ - - - - Debug - AnyCPU - {C8298223-2468-466C-9B06-FBF61926CECB} - Library - Properties - Emby.XmlTv.Test - Emby.XmlTv.Test - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - - ..\ - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - False - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - Designer - - - - - {0D023565-5942-4D79-9098-A1B4B6665A40} - Emby.XmlTv - - - - - - - False - - - False - - - False - - - False - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - netstandard2.0 - false - - diff --git a/Emby.XmlTv/Emby.XmlTv.Test/Properties/AssemblyInfo.cs b/Emby.XmlTv/Emby.XmlTv.Test/Properties/AssemblyInfo.cs deleted file mode 100644 index 9028603635..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Emby.XmlTv.Test")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Emby.XmlTv.Test")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("c8298223-2468-466c-9b06-fbf61926cecb")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Emby.XmlTv/Emby.XmlTv.Test/XmlTvReaderDateTimeTests.cs b/Emby.XmlTv/Emby.XmlTv.Test/XmlTvReaderDateTimeTests.cs deleted file mode 100644 index 857cc339c0..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Test/XmlTvReaderDateTimeTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.IO; - -using Emby.XmlTv.Classes; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Emby.XmlTv.Test -{ - [TestClass] - public class XmlTvReaderDateTimeTests - { - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_HandlePartDates() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, "es"); - - Assert.AreEqual(Parse("01 Jan 2016 00:00:00"), reader.ParseDate("2016")); - Assert.AreEqual(Parse("01 Jan 2016 00:00:00"), reader.ParseDate("201601")); - Assert.AreEqual(Parse("01 Jan 2016 00:00:00"), reader.ParseDate("20160101")); - Assert.AreEqual(Parse("01 Jan 2016 12:00:00"), reader.ParseDate("2016010112")); - Assert.AreEqual(Parse("01 Jan 2016 12:34:00"), reader.ParseDate("201601011234")); - Assert.AreEqual(Parse("01 Jan 2016 12:34:56"), reader.ParseDate("20160101123456")); - } - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_HandleDateWithOffset() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, "es"); - - // parse variations on 1:00AM - Assert.AreEqual(Parse("01 Jan 2016 12:00:00"), reader.ParseDate("20160101120000 +0000")); - Assert.AreEqual(Parse("01 Jan 2016 02:00:00"), reader.ParseDate("20160101120000 +1000")); - Assert.AreEqual(Parse("01 Jan 2016 11:00:00"), reader.ParseDate("20160101120000 +0100")); - Assert.AreEqual(Parse("01 Jan 2016 11:50:00"), reader.ParseDate("20160101120000 +0010")); - Assert.AreEqual(Parse("01 Jan 2016 11:59:00"), reader.ParseDate("20160101120000 +0001")); - - Assert.AreEqual(Parse("01 Jan 2016 22:00:00"), reader.ParseDate("20160101120000 -1000")); - Assert.AreEqual(Parse("01 Jan 2016 13:00:00"), reader.ParseDate("20160101120000 -0100")); - Assert.AreEqual(Parse("01 Jan 2016 12:10:00"), reader.ParseDate("20160101120000 -0010")); - Assert.AreEqual(Parse("01 Jan 2016 12:01:00"), reader.ParseDate("20160101120000 -0001")); - } - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_HandlePartDatesWithOffset() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, "es"); - - Assert.AreEqual(Parse("01 Jan 2016 01:00:00"), reader.ParseDate("2016 -0100")); - Assert.AreEqual(Parse("01 Jan 2016 01:00:00"), reader.ParseDate("201601 -0100")); - Assert.AreEqual(Parse("01 Jan 2016 01:00:00"), reader.ParseDate("20160101 -0100")); - Assert.AreEqual(Parse("01 Jan 2016 13:00:00"), reader.ParseDate("2016010112 -0100")); - Assert.AreEqual(Parse("01 Jan 2016 13:00:00"), reader.ParseDate("201601011200 -0100")); - Assert.AreEqual(Parse("01 Jan 2016 13:00:00"), reader.ParseDate("20160101120000 -0100")); - } - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_HandleSpaces() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, "es"); - - // parse variations on 1:00AM - Assert.AreEqual(Parse("01 Jan 2016 12:00:00"), reader.ParseDate("20160101120000 +000")); - Assert.AreEqual(Parse("01 Jan 2016 12:00:00"), reader.ParseDate("20160101120000 +00")); - Assert.AreEqual(Parse("01 Jan 2016 12:00:00"), reader.ParseDate("20160101120000 +0")); - } - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_HandleSpaces2() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, "es"); - - // parse variations on 1:00AM - Assert.AreEqual(Parse("01 Jan 2016 12:00:00"), reader.ParseDate("20160101120000 0")); - } - - private DateTimeOffset Parse(string value) - { - return new DateTimeOffset(DateTimeOffset.Parse(value).Ticks, TimeSpan.Zero); - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv.Test/XmlTvReaderLanguageTests.cs b/Emby.XmlTv/Emby.XmlTv.Test/XmlTvReaderLanguageTests.cs deleted file mode 100644 index 32a0431d28..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Test/XmlTvReaderLanguageTests.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; - -using Emby.XmlTv.Classes; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Emby.XmlTv.Test -{ - [TestClass] - public class XmlTvReaderLanguageTests - { - /* Homes Under the Hammer - Spanish - * Homes Under the Hammer - Spanish 2 - * Homes Under the Hammer - English - * Homes Under the Hammer - English 2 - * Homes Under the Hammer - Empty Language - * Homes Under the Hammer - Empty Language 2 - * Homes Under the Hammer - No Language - * Homes Under the Hammer - No Language 2 - */ - - /* Expected Behaviour: - * - Language = Null Homes Under the Hammer - No Language - * - Language = "" Homes Under the Hammer - No Language - * - Language = es Homes Under the Hammer - Spanish - * - Language = en Homes Under the Hammer - English - */ - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_Return_The_First_Matching_Language_ES() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, "es"); - var channel = reader.GetChannels().FirstOrDefault(); - Assert.IsNotNull(channel); - - var startDate = new DateTime(2015, 11, 26); - var cancellationToken = new CancellationToken(); - var programme = reader.GetProgrammes(channel.Id, startDate, startDate.AddDays(1), cancellationToken).FirstOrDefault(); - - Assert.IsNotNull(programme); - Assert.AreEqual("Homes Under the Hammer - Spanish", programme.Title); - Assert.AreEqual(1, programme.Categories.Count); - Assert.AreEqual("Property - Spanish", programme.Categories[0]); - } - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_Return_The_First_Matching_Language_EN() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, "en"); - - var channel = reader.GetChannels().FirstOrDefault(); - Assert.IsNotNull(channel); - - var startDate = new DateTime(2015, 11, 26); - var cancellationToken = new CancellationToken(); - var programme = reader.GetProgrammes(channel.Id, startDate, startDate.AddDays(1), cancellationToken).FirstOrDefault(); - - Assert.IsNotNull(programme); - Assert.AreEqual("Homes Under the Hammer - English", programme.Title); - Assert.AreEqual(1, programme.Categories.Count); - Assert.AreEqual("Property - English", programme.Categories[0]); - } - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_Return_The_First_Matching_With_No_Language() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, null); - - var channel = reader.GetChannels().FirstOrDefault(); - Assert.IsNotNull(channel); - - var startDate = new DateTime(2015, 11, 26); - var cancellationToken = new CancellationToken(); - var programme = reader.GetProgrammes(channel.Id, startDate, startDate.AddDays(1), cancellationToken).FirstOrDefault(); - - Assert.IsNotNull(programme); - Assert.AreEqual("Homes Under the Hammer - No Language", programme.Title); - Assert.AreEqual(1, programme.Categories.Count); - Assert.AreEqual("Property - No Language", programme.Categories[0]); - } - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_Return_The_First_Matching_With_Empty_Language() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, String.Empty); - - var channel = reader.GetChannels().FirstOrDefault(); - Assert.IsNotNull(channel); - - var startDate = new DateTime(2015, 11, 26); - var cancellationToken = new CancellationToken(); - var programme = reader.GetProgrammes(channel.Id, startDate, startDate.AddDays(1), cancellationToken).FirstOrDefault(); - - Assert.IsNotNull(programme); - Assert.AreEqual("Homes Under the Hammer - Empty Language", programme.Title); - Assert.AreEqual(1, programme.Categories.Count); - Assert.AreEqual("Property - Empty Language", programme.Categories[0]); - } - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_Return_The_First_When_NoMatchFound() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, "es"); // There are no titles or categories for spanish - - var channel = reader.GetChannels().FirstOrDefault(); - Assert.IsNotNull(channel); - - var startDate = new DateTime(2015, 11, 26); - var cancellationToken = new CancellationToken(); - var programme = reader.GetProgrammes(channel.Id, startDate, startDate.AddDays(1), cancellationToken).Skip(1).FirstOrDefault(); - - Assert.IsNotNull(programme); - Assert.AreEqual("Homes Under the Hammer - English", programme.Title); - - // Should return all categories - Assert.AreEqual(2, programme.Categories.Count); - Assert.IsTrue(programme.Categories.Contains("Property - English")); - Assert.IsTrue(programme.Categories.Contains("Property - Empty Language")); - } - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_Return_The_First_When_NoLanguage() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile, null); - - var channel = reader.GetChannels().FirstOrDefault(); - Assert.IsNotNull(channel); - - var startDate = new DateTime(2015, 11, 26); - var cancellationToken = new CancellationToken(); - var programme = reader.GetProgrammes(channel.Id, startDate, startDate.AddDays(1), cancellationToken).Skip(1).FirstOrDefault(); - - Assert.IsNotNull(programme); - Assert.AreEqual("Homes Under the Hammer - English", programme.Title); // Should return the first in the list - - // Should return all categories - Assert.AreEqual(2, programme.Categories.Count); - Assert.IsTrue(programme.Categories.Contains("Property - English")); - Assert.IsTrue(programme.Categories.Contains("Property - Empty Language")); - } - - [TestMethod] - [DeploymentItem("Xml Files\\MultilanguageData.xml")] - public void Should_Return_All_Languages() - { - var testFile = Path.GetFullPath(@"MultilanguageData.xml"); - var reader = new XmlTvReader(testFile); - var cancellationToken = new CancellationToken(); - - var results = reader.GetLanguages(cancellationToken); - Assert.IsNotNull(results); - - foreach (var result in results) - { - Console.WriteLine("{0} - {1}", result.Name, result.Relevance); - } - - Assert.AreEqual(2, results.Count); - Assert.AreEqual("en", results[0].Name); - Assert.AreEqual(11, results[0].Relevance); - Assert.AreEqual("es", results[1].Name); - Assert.AreEqual(3, results[1].Relevance); - } - - } -} diff --git a/Emby.XmlTv/Emby.XmlTv.Test/XmlTvReaderTests.cs b/Emby.XmlTv/Emby.XmlTv.Test/XmlTvReaderTests.cs deleted file mode 100644 index b1dffc59cd..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.Test/XmlTvReaderTests.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; - -using Emby.XmlTv.Classes; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Emby.XmlTv.Test -{ - [TestClass] - public class XmlTvReaderTests - { - [TestMethod] - [DeploymentItem("Xml Files\\UK_Data.xml")] - public void UK_DataTest_ChannelsTest() - { - var testFile = Path.GetFullPath(@"UK_Data.xml"); - var reader = new XmlTvReader(testFile); - - var channels = reader.GetChannels().ToList(); - Assert.AreEqual(5, channels.Count); - - // Check each channel - var channel = channels.SingleOrDefault(c => c.Id == "UK_RT_2667"); - Assert.IsNotNull(channel); - Assert.AreEqual("BBC1 HD", channel.DisplayName); - Assert.AreEqual("7.1", channel.Number); - Assert.IsNotNull(channel.Icon); - Assert.AreEqual("Logo_UK_RT_2667", channel.Icon.Source); - Assert.AreEqual(100, channel.Icon.Width); - Assert.AreEqual(200, channel.Icon.Height); - - channel = channels.SingleOrDefault(c => c.Id == "UK_RT_105"); - Assert.IsNotNull(channel); - Assert.AreEqual("BBC2", channel.DisplayName); - Assert.IsNotNull(channel.Icon); - Assert.AreEqual("Logo_UK_RT_105", channel.Icon.Source); - Assert.IsFalse(channel.Icon.Width.HasValue); - Assert.IsFalse(channel.Icon.Height.HasValue); - - channel = channels.SingleOrDefault(c => c.Id == "UK_RT_2118"); - Assert.IsNotNull(channel); - Assert.AreEqual("ITV1 HD", channel.DisplayName); - Assert.IsNotNull(channel.Icon); - Assert.AreEqual("Logo_UK_RT_2118", channel.Icon.Source); - Assert.AreEqual(100, channel.Icon.Width); - Assert.IsFalse(channel.Icon.Height.HasValue); - - channel = channels.SingleOrDefault(c => c.Id == "UK_RT_2056"); - Assert.IsNotNull(channel); - Assert.AreEqual("Channel 4 HD", channel.DisplayName); - Assert.IsNotNull(channel.Icon); - Assert.AreEqual("Logo_UK_RT_2056", channel.Icon.Source); - Assert.IsFalse(channel.Icon.Width.HasValue); - Assert.AreEqual(200, channel.Icon.Height); - - channel = channels.SingleOrDefault(c => c.Id == "UK_RT_134"); - Assert.IsNotNull(channel); - Assert.AreEqual("Channel 5", channel.DisplayName); - Assert.IsNull(channel.Icon); - } - - [TestMethod] - [DeploymentItem("Xml Files\\UK_Data.xml")] - public void UK_DataTest_GeneralTest() - { - var testFile = Path.GetFullPath(@"UK_Data.xml"); - var reader = new XmlTvReader(testFile, null); - - var channels = reader.GetChannels().ToList(); - Assert.AreEqual(5, channels.Count); - - // Pick a channel to check the data for - var channel = channels.SingleOrDefault(c => c.Id == "UK_RT_2056"); - Assert.IsNotNull(channel); - - var startDate = new DateTime(2015, 11, 26); - var cancellationToken = new CancellationToken(); - var programmes = reader.GetProgrammes(channel.Id, startDate, startDate.AddDays(1), cancellationToken).ToList(); - - Assert.AreEqual(27, programmes.Count); - var programme = programmes.SingleOrDefault(p => p.Title == "The Secret Life of"); - - Assert.IsNotNull(programme); - Assert.AreEqual(new DateTime(2015, 11, 26, 20, 0, 0), programme.StartDate); - Assert.AreEqual(new DateTime(2015, 11, 26, 21, 0, 0), programme.EndDate); - Assert.AreEqual("Cameras follow the youngsters' development after two weeks apart and time has made the heart grow fonder for Alfie and Emily, who are clearly happy to be back together. And although Alfie struggled to empathise with the rest of his peers before, a painting competition proves to be a turning point for him. George takes the children's rejection of his family recipe to heart, but goes on to triumph elsewhere, and romance is in the air when newcomer Sienna captures Arthur's heart.", programme.Description); - Assert.AreEqual("Documentary", programme.Categories.Single()); - Assert.IsNotNull(programme.Episode); - Assert.AreEqual("The Secret Life of 5 Year Olds", programme.Episode.Title); - Assert.AreEqual(1, programme.Episode.Series); - Assert.IsNull(programme.Episode.SeriesCount); - Assert.AreEqual(4, programme.Episode.Episode); - Assert.AreEqual(6, programme.Episode.EpisodeCount); - Assert.IsNotNull(programme.Premiere); - //Assert.AreEqual("First showing on national terrestrial TV", programme.Premiere.Details); - Assert.IsTrue(programme.IsNew); - } - - [TestMethod] - [DeploymentItem("Xml Files\\UK_Data.xml")] - public void UK_DataTest_MultipleTitles_SameLanguage_Should_ReturnFirstValue() - { - var testFile = Path.GetFullPath(@"UK_Data.xml"); - var reader = new XmlTvReader(testFile, null); - - /* - Homes Under the Hammer - Title 1 - Homes Under the Hammer - Title 2 - Homes Under the Hammer - Title 3 - */ - - var startDate = new DateTime(2015, 11, 26); - var cancellationToken = new CancellationToken(); - var programmes = reader.GetProgrammes("UK_RT_2667", startDate, startDate.AddDays(1), cancellationToken).ToList(); - var programme = programmes.SingleOrDefault(p => p.Title == "Homes Under the Hammer - Title 1"); - - Assert.IsNotNull(programme); - } - - [TestMethod] - [DeploymentItem("Xml Files\\UK_Data.xml")] - public void UK_DataTest_MultipleTitles_NoLanguage_Should_ReturnFirstValue() - { - var testFile = Path.GetFullPath(@"UK_Data.xml"); - var reader = new XmlTvReader(testFile, null); - - /* - Oxford Street Revealed - Title 1 - Oxford Street Revealed - Title 2 - Oxford Street Revealed - Title 3 - */ - - var startDate = new DateTime(2015, 11, 26); - var cancellationToken = new CancellationToken(); - var programmes = reader.GetProgrammes("UK_RT_2667", startDate, startDate.AddDays(1), cancellationToken).ToList(); - var programme = programmes.SingleOrDefault(p => p.Title == "Oxford Street Revealed - Title 1"); - - Assert.IsNotNull(programme); - } - - [TestMethod] - [DeploymentItem("Xml Files\\ES_MultiLanguageData.xml")] - public void ES_MultiLanguageDataTest() - { - var testFile = Path.GetFullPath(@"ES_MultiLanguageData.xml"); - var reader = new XmlTvReader(testFile, "es"); // Specify the spanish language explicitly - - var channels = reader.GetChannels().ToList(); - Assert.AreEqual(141, channels.Count); - - // Pick a channel to check the data for - var channel = channels.SingleOrDefault(c => c.Id == "Canal + HD" && c.DisplayName == "Canal + HD"); - Assert.IsNotNull(channel); - - var startDate = new DateTime(2016, 02, 18); - var cancellationToken = new CancellationToken(); - var programmes = reader.GetProgrammes(channel.Id, startDate, startDate.AddDays(1), cancellationToken).ToList(); - - Assert.AreEqual(22, programmes.Count); - var programme = programmes.SingleOrDefault(p => p.Title == "This is Comedy. Judd Apatow & Co."); - - /* - - This is Comedy. Judd Apatow & Co. - This is Comedy - El resurgir creativo de la comedia estadounidense en los últimos 15 años ha tenido un nombre indiscutible, Judd Apatow, y unos colaboradores indispensables, sus amigos (actores, cómicos, escritores) Jonah Hill, Steve Carrell, Paul Rudd, Seth Rogen, Lena Dunham... A través de extractos de sus filmes y de entrevistas a algunos los miembros de su 'banda' (Adam Sandler, Lena Dunham o Jason Segel), este documental muestra la carrera de un productor y director excepcional que ha sido capaz de llevar la risa a su máxima expresión - - Jacky Goldberg - - 2014 - Documentales - Sociedad - - Francia - - TV-G - - - 3/5 - - - */ - - Assert.IsNotNull(programme); - Assert.AreEqual(new DateTime(2016, 02, 18, 4, 51, 0), programme.StartDate); - Assert.AreEqual(new DateTime(2016, 02, 18, 5, 54, 0), programme.EndDate); - Assert.AreEqual("El resurgir creativo de la comedia estadounidense en los últimos 15 años ha tenido un nombre indiscutible, Judd Apatow, y unos colaboradores indispensables, sus amigos (actores, cómicos, escritores) Jonah Hill, Steve Carrell, Paul Rudd, Seth Rogen, Lena Dunham... A través de extractos de sus filmes y de entrevistas a algunos los miembros de su 'banda' (Adam Sandler, Lena Dunham o Jason Segel), este documental muestra la carrera de un productor y director excepcional que ha sido capaz de llevar la risa a su máxima expresión", programme.Description); - Assert.AreEqual(2, programme.Categories.Count); - Assert.AreEqual("Documentales", programme.Categories[0]); - Assert.AreEqual("Sociedad", programme.Categories[1]); - Assert.IsNotNull(programme.Episode); - Assert.IsNull(programme.Episode.Episode); - Assert.IsNull(programme.Episode.EpisodeCount); - Assert.IsNull(programme.Episode.Part); - Assert.IsNull(programme.Episode.PartCount); - Assert.IsNull(programme.Episode.Series); - Assert.IsNull(programme.Episode.SeriesCount); - Assert.IsNull(programme.Episode.Title); - } - - [TestMethod] - [DeploymentItem("Xml Files\\honeybee.xml")] - public void HoneybeeTest() - { - var testFile = Path.GetFullPath(@"honeybee.xml"); - var reader = new XmlTvReader(testFile, null); - - var channels = reader.GetChannels().ToList(); - Assert.AreEqual(16, channels.Count); - - var programs = reader.GetProgrammes("2013.honeybee.it", DateTime.UtcNow.AddYears(-1), - DateTime.UtcNow.AddYears(1), CancellationToken.None).ToList(); - Assert.AreEqual(297, programs.Count); - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv.sln b/Emby.XmlTv/Emby.XmlTv.sln deleted file mode 100644 index 8243d4b72b..0000000000 --- a/Emby.XmlTv/Emby.XmlTv.sln +++ /dev/null @@ -1,44 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.XmlTv.Console", "Emby.XmlTv.Console\Emby.XmlTv.Console.csproj", "{ADD1D993-6D74-480A-B1FC-7FD9FD05A769}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.XmlTv", "Emby.XmlTv\Emby.XmlTv.csproj", "{0D023565-5942-4D79-9098-A1B4B6665A40}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{BB0FD191-A9D0-4CC9-A79E-ECBCF1275268}" - ProjectSection(SolutionItems) = preProject - .nuget\NuGet.Config = .nuget\NuGet.Config - .nuget\NuGet.exe = .nuget\NuGet.exe - .nuget\NuGet.targets = .nuget\NuGet.targets - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution Files", "{E9F625D5-979E-48E8-9987-F4BCADD79A41}" - ProjectSection(SolutionItems) = preProject - Nuget\Emby.XmlTv.nuspec = Nuget\Emby.XmlTv.nuspec - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {ADD1D993-6D74-480A-B1FC-7FD9FD05A769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ADD1D993-6D74-480A-B1FC-7FD9FD05A769}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ADD1D993-6D74-480A-B1FC-7FD9FD05A769}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ADD1D993-6D74-480A-B1FC-7FD9FD05A769}.Release|Any CPU.Build.0 = Release|Any CPU - {0D023565-5942-4D79-9098-A1B4B6665A40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D023565-5942-4D79-9098-A1B4B6665A40}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D023565-5942-4D79-9098-A1B4B6665A40}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D023565-5942-4D79-9098-A1B4B6665A40}.Release|Any CPU.Build.0 = Release|Any CPU - {C8298223-2468-466C-9B06-FBF61926CECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C8298223-2468-466C-9B06-FBF61926CECB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C8298223-2468-466C-9B06-FBF61926CECB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C8298223-2468-466C-9B06-FBF61926CECB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs b/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs deleted file mode 100644 index 46bf6cc21c..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Classes/XmlTvReader.cs +++ /dev/null @@ -1,1107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using System.Xml; -using Emby.XmlTv.Entities; - -namespace Emby.XmlTv.Classes -{ - // Reads an XmlTv file - public class XmlTvReader - { - private readonly string _fileName; - private readonly string _language; - - /// - /// Initializes a new instance of the class. - /// - /// Name of the file. - /// The specific language to return. - public XmlTvReader(string fileName, string language = null) - { - _fileName = fileName; - - // Normalize null/string.empty - if (string.IsNullOrWhiteSpace(language)) - { - language = null; - } - _language = language; - } - - private static XmlReader CreateXmlTextReader(string path) - { - var settings = new XmlReaderSettings(); - - // https://msdn.microsoft.com/en-us/library/system.xml.xmlreadersettings.xmlresolver(v=vs.110).aspx - // Looks like we don't need this anyway? - // Starting with the .NET Framework 4.5.2, this setting has a default value of null. - //settings.XmlResolver = null; - - settings.DtdProcessing = DtdProcessing.Ignore; - - settings.CheckCharacters = false; - settings.IgnoreProcessingInstructions = true; - settings.IgnoreComments = true; - //settings.ValidationType = ValidationType.None; - - return XmlReader.Create(path, settings); - } - - /// - /// Gets the list of channels present in the XML - /// - /// - public IEnumerable GetChannels() - { - var list = new List(); - - using (var reader = CreateXmlTextReader(_fileName)) - { - if (reader.ReadToDescendant("tv")) - { - if (reader.ReadToDescendant("channel")) - { - do - { - var channel = GetChannel(reader); - if (channel != null) - { - list.Add(channel); - } - } - while (reader.ReadToFollowing("channel")); - } - } - } - - return list; - } - - private XmlTvChannel GetChannel(XmlReader reader) - { - var id = reader.GetAttribute("id"); - - if (string.IsNullOrEmpty(id)) - { - // LogError("No id found for channel row"); - // Log.Error(" channel#{0} doesnt contain an id", iChannel); - return null; - } - - var result = new XmlTvChannel() { Id = id }; - - using (var xmlChannel = reader.ReadSubtree()) - { - xmlChannel.MoveToContent(); - xmlChannel.Read(); - - // Read out the data for each node and process individually - while (!xmlChannel.EOF && xmlChannel.ReadState == ReadState.Interactive) - { - if (xmlChannel.NodeType == XmlNodeType.Element) - { - switch (xmlChannel.Name) - { - case "display-name": - ProcessNode(xmlChannel, s => result.DisplayName = s, _language, s => SetChannelNumber(result, s)); - break; - case "url": - result.Url = xmlChannel.ReadElementContentAsString(); - break; - case "icon": - result.Icon = ProcessIconNode(xmlChannel); - xmlChannel.Skip(); - break; - default: - xmlChannel.Skip(); // unknown, skip entire node - break; - } - } - else - { - xmlChannel.Read(); - } - } - } - - if (string.IsNullOrEmpty(result.DisplayName)) - { - // LogError("No display-name found for channel {0}", id); - return null; - } - - return result; - } - - private void SetChannelNumber(XmlTvChannel channel, string value) - { - value = value.Replace("-", "."); - if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) - { - channel.Number = value; - } - } - - /// - /// Gets the programmes for a specified channel - /// - /// The channel id. - /// The UTC start date. - /// The UTC end date. - /// A cancellation token. - /// - public IEnumerable GetProgrammes( - string channelId, - DateTimeOffset startDateUtc, - DateTimeOffset endDateUtc, - CancellationToken cancellationToken) - { - var list = new List(); - - using (var reader = CreateXmlTextReader(_fileName)) - { - if (reader.ReadToDescendant("tv")) - { - if (reader.ReadToDescendant("programme")) - { - do - { - if (cancellationToken.IsCancellationRequested) - { - continue; // Break out - } - - var programme = GetProgramme(reader, channelId, startDateUtc, endDateUtc); - if (programme != null) - { - list.Add(programme); - } - } - while (reader.ReadToFollowing("programme")); - } - } - } - - return list; - } - - public XmlTvProgram GetProgramme(XmlReader reader, string channelId, DateTimeOffset startDateUtc, DateTimeOffset endDateUtc) - { - var result = new XmlTvProgram(); - - PopulateHeader(reader, result); - - using (var xmlProg = reader.ReadSubtree()) - { - // First up, validate that this is the correct channel, and programme is within the time we are expecting - if (!string.Equals(result.ChannelId, channelId, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - if (result.EndDate < startDateUtc || result.StartDate >= endDateUtc) - { - return null; - } - - xmlProg.MoveToContent(); - xmlProg.Read(); - - // Loop through each element - while (!xmlProg.EOF && xmlProg.ReadState == ReadState.Interactive) - { - if (xmlProg.NodeType == XmlNodeType.Element) - { - switch (xmlProg.Name) - { - case "title": - ProcessTitleNode(xmlProg, result); - break; - case "category": - ProcessCategory(xmlProg, result); - break; - case "country": - ProcessCountry(xmlProg, result); - break; - case "desc": - ProcessDescription(xmlProg, result); - break; - case "sub-title": - ProcessSubTitle(xmlProg, result); - break; - case "new": - ProcessNew(xmlProg, result); - break; - case "previously-shown": - ProcessPreviouslyShown(xmlProg, result); - break; - case "quality": - ProcessQuality(xmlProg, result); - break; - case "episode-num": - ProcessEpisodeNum(xmlProg, result); - break; - case "date": // Copyright date - ProcessCopyrightDate(xmlProg, result); - break; - case "star-rating": // Community Rating - ProcessStarRating(xmlProg, result); - break; - case "rating": // Certification Rating - ProcessRating(xmlProg, result); - break; - case "credits": - if (xmlProg.IsEmptyElement) - { - xmlProg.Skip(); - } - else - { - using (var subtree = xmlProg.ReadSubtree()) - { - ProcessCredits(subtree, result); - } - } - break; - case "icon": - result.Icon = ProcessIconNode(xmlProg); - xmlProg.Skip(); - break; - case "premiere": - result.Premiere = new XmlTvPremiere(); - // This was causing data after the premiere node to not be read. Reactivate this and debug if the premiere details are ever needed. - //ProcessPremiereNode(xmlProg, result); - xmlProg.Skip(); - break; - default: - // unknown, skip entire node - xmlProg.Skip(); - break; - } - } - else - { - xmlProg.Read(); - } - } - } - return result; - } - - /// - /// Gets the list of supported languages in the XML - /// - /// - public List GetLanguages(CancellationToken cancellationToken) - { - var results = new Dictionary(); - - //Loop through and parse out all elements and then lang= attributes - //logger.LogInformation("Loading file {0}", _fileName); - using (var reader = CreateXmlTextReader(_fileName)) - { - while (reader.Read()) - { - if (cancellationToken.IsCancellationRequested) - { - continue; // Break out - } - - if (reader.NodeType == XmlNodeType.Element) - { - var language = reader.GetAttribute("lang"); - if (!string.IsNullOrEmpty(language)) - { - if (!results.ContainsKey(language)) - { - results[language] = 0; - } - results[language]++; - } - } - } - } - - return - results.Keys.Select(k => new XmlTvLanguage() { Name = k, Relevance = results[k] }) - .OrderByDescending(l => l.Relevance) - .ToList(); - } - - private void ProcessCopyrightDate(XmlReader xmlProg, XmlTvProgram result) - { - var startValue = xmlProg.ReadElementContentAsString(); - if (string.IsNullOrEmpty(startValue)) - { - // Log.Error(" programme#{0} doesnt contain a start date", iChannel); - result.CopyrightDate = null; - } - else - { - var copyrightDate = ParseDate(startValue); - if (copyrightDate != null) - { - result.CopyrightDate = copyrightDate; - } - } - } - - public void ProcessCredits(XmlReader creditsXml, XmlTvProgram result) - { - creditsXml.MoveToContent(); - creditsXml.Read(); - - // Loop through each element - while (!creditsXml.EOF && creditsXml.ReadState == ReadState.Interactive) - { - if (creditsXml.NodeType == XmlNodeType.Element) - { - XmlTvCredit credit = null; - switch (creditsXml.Name) - { - case "director": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Director }; - break; - case "actor": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Actor }; - break; - case "writer": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Writer }; - break; - case "adapter": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Adapter }; - break; - case "producer": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Producer }; - break; - case "composer": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Composer }; - break; - case "editor": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Editor }; - break; - case "presenter": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Presenter }; - break; - case "commentator": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Commentator }; - break; - case "guest": - credit = new XmlTvCredit() { Type = XmlTvCreditType.Guest }; - break; - } - - if (credit != null) - { - credit.Name = creditsXml.ReadElementContentAsString(); - result.Credits.Add(credit); - } - else - { - creditsXml.Skip(); - } - } - else - creditsXml.Read(); - } - } - - public void ProcessStarRating(XmlReader reader, XmlTvProgram result) - { - /* - - 3/3 - - */ - - reader.ReadToDescendant("value"); - if (reader.Name == "value") - { - var textValue = reader.ReadElementContentAsString(); - if (textValue.Contains("/")) - { - var components = textValue.Split('/'); - if (float.TryParse(components[0], out var value)) - { - result.StarRating = value; - } - } - } - else - { - reader.Skip(); - } - } - - public void ProcessRating(XmlReader reader, XmlTvProgram result) - { - /* - - TV-G - - */ - - var system = reader.GetAttribute("system"); - - reader.ReadToDescendant("value"); - if (reader.Name == "value") - { - result.Rating = new XmlTvRating() - { - System = system, - Value = reader.ReadElementContentAsString() - }; - } - else - { - reader.Skip(); - } - } - - public void ProcessEpisodeNum(XmlReader reader, XmlTvProgram result) - { - /* - EP00003026.0666 - 2706 - .26/0. - */ - - var episodeSystem = reader.GetAttribute("system"); - switch (episodeSystem) - { - case "dd_progid": - ParseEpisodeDataForProgramId(reader, result); - break; - case "icetv": - result.ProviderIds["icetv"] = reader.ReadElementContentAsString(); - break; - case "xmltv_ns": - ParseEpisodeDataForXmlTvNs(reader, result); - break; - case "onscreen": - ParseEpisodeDataForOnScreen(reader, result); - break; - case "thetvdb.com": - ParseTvdbSystem(reader, result); - break; - case "imdb.com": - ParseImdbSystem(reader, result); - break; - case "themoviedb.org": - ParseMovieDbSystem(reader, result); - break; - case "SxxExx": - ParseSxxExxSystem(reader, result); - break; - default: // Handles empty string and nulls - reader.Skip(); - break; - } - } - - public void ParseSxxExxSystem(XmlReader reader, XmlTvProgram result) - { - // S012E32 - - var value = reader.ReadElementContentAsString(); - var res = Regex.Match(value, "s([0-9]+)e([0-9]+)", RegexOptions.IgnoreCase); - - if (res.Success) - { - int parsedInt; - - if (int.TryParse(res.Groups[1].Value, out parsedInt)) - { - result.Episode.Series = parsedInt; - } - - if (int.TryParse(res.Groups[2].Value, out parsedInt)) - { - result.Episode.Episode = parsedInt; - } - } - } - - public void ParseMovieDbSystem(XmlReader reader, XmlTvProgram result) - { - // series/248841 - // episode/4749206 - - var value = reader.ReadElementContentAsString(); - var parts = value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - - if (string.Equals(parts[0], "series", StringComparison.OrdinalIgnoreCase)) - { - result.SeriesProviderIds["tmdb"] = parts[1]; - } - - else if (parts.Length == 1 || string.Equals(parts[0], "episode", StringComparison.OrdinalIgnoreCase)) - { - result.ProviderIds["tmdb"] = parts.Last(); - } - } - - public void ParseImdbSystem(XmlReader reader, XmlTvProgram result) - { - // series/tt1837576 - // episode/tt3288596 - - var value = reader.ReadElementContentAsString(); - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - var parts = value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length != 2) - { - return; - } - - if (string.Equals(parts[0], "series", StringComparison.OrdinalIgnoreCase)) - { - result.SeriesProviderIds["imdb"] = parts[1]; - } - - else if (string.Equals(parts[0], "episode", StringComparison.OrdinalIgnoreCase)) - { - result.ProviderIds["imdb"] = parts[1]; - } - } - - public void ParseTvdbSystem(XmlReader reader, XmlTvProgram result) - { - // series/248841 - // episode/4749206 - - var value = reader.ReadElementContentAsString(); - var parts = value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length != 2) - { - return; - } - - if (string.Equals(parts[0], "series", StringComparison.OrdinalIgnoreCase)) - { - result.SeriesProviderIds["tvdb"] = parts[1]; - } - - else if (string.Equals(parts[0], "episode", StringComparison.OrdinalIgnoreCase)) - { - result.ProviderIds["tvdb"] = parts[1]; - } - } - - public void ParseEpisodeDataForOnScreen(XmlReader reader, XmlTvProgram result) - { - //// example: 'Episode #FFEE' - //serEpNum = ConvertHTMLToAnsi(nodeEpisodeNum); - //int num1 = serEpNum.IndexOf("#", 0); - //if (num1 < 0) num1 = 0; - //episodeNum = CorrectEpisodeNum(serEpNum.Substring(num1, serEpNum.Length - num1), 0); - - var value = reader.ReadElementContentAsString(); - // value = HttpUtility.HtmlDecode(value); - value = value.Replace(" ", ""); - - var hashIndex = value.IndexOf("#", StringComparison.Ordinal); - if (hashIndex > -1) - { - // Take everything from the hash to the end. - //TODO: This could be textual - how do we populate an Int32 - // result.EpisodeNumber - } - } - - public void ParseEpisodeDataForProgramId(XmlReader reader, XmlTvProgram result) - { - var value = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(value)) - { - result.ProgramId = value; - } - } - - public void ParseEpisodeDataForXmlTvNs(XmlReader reader, XmlTvProgram result) - { - var value = reader.ReadElementContentAsString(); - - value = value.Replace(" ", ""); - - // Episode details - var components = value.Split(new[] { "." }, StringSplitOptions.None); - - int parsedInt; - - if (!string.IsNullOrEmpty(components[0])) - { - // Handle either "5/12" or "5" - var seriesComponents = components[0].Split(new[] { "/" }, StringSplitOptions.None); - - // handle the zero basing! - if (int.TryParse(seriesComponents[0], out parsedInt)) - { - result.Episode.Series = parsedInt + 1; - if (seriesComponents.Length == 2) - { - if (int.TryParse(seriesComponents[1], out parsedInt)) - { - result.Episode.SeriesCount = parsedInt; - } - } - } - } - - if (components.Length >= 2) - { - if (!string.IsNullOrEmpty(components[1])) - { - // Handle either "5/12" or "5" - var episodeComponents = components[1].Split(new[] { "/" }, StringSplitOptions.None); - - // handle the zero basing! - if (int.TryParse(episodeComponents[0], out parsedInt)) - { - result.Episode.Episode = parsedInt + 1; - if (episodeComponents.Count() == 2) - { - if (int.TryParse(episodeComponents[1], out parsedInt)) - { - result.Episode.EpisodeCount = parsedInt; - } - } - } - } - } - - if (components.Length >= 3) - { - if (!string.IsNullOrEmpty(components[2])) - { - // Handle either "5/12" or "5" - var partComponents = components[2].Split(new[] { "/" }, StringSplitOptions.None); - - // handle the zero basing! - if (int.TryParse(partComponents[0], out parsedInt)) - { - result.Episode.Part = parsedInt + 1; - if (partComponents.Count() == 2) - { - if (int.TryParse(partComponents[1], out parsedInt)) - { - result.Episode.PartCount = parsedInt; - } - } - } - } - } - } - - public void ProcessQuality(XmlReader reader, XmlTvProgram result) - { - result.Quality = reader.ReadElementContentAsString(); - } - - public void ProcessPreviouslyShown(XmlReader reader, XmlTvProgram result) - { - // - var value = reader.GetAttribute("start"); - if (!string.IsNullOrEmpty(value)) - { - // TODO: this may not be correct = validate it - result.PreviouslyShown = ParseDate(value); - if (result.PreviouslyShown != result.StartDate) - { - result.IsPreviouslyShown = true; - } - } - else - { - result.IsPreviouslyShown = true; - } - - reader.Skip(); // Move on - } - - public void ProcessNew(XmlReader reader, XmlTvProgram result) - { - result.IsNew = true; - reader.Skip(); // Move on - } - - public void ProcessCategory(XmlReader reader, XmlTvProgram result) - { - /* - News - */ - - result.Categories = result.Categories ?? new List(); - ProcessMultipleNodes(reader, s => result.Categories.Add(s), _language); - - //result.Categories.Add(reader.ReadElementContentAsString()); - } - public void ProcessCountry(XmlReader reader, XmlTvProgram result) - { - /* - Canadá - EE.UU - */ - - result.Countries = result.Countries ?? new List(); - ProcessNode(reader, s => result.Countries.Add(s), _language); - } - - public void ProcessSubTitle(XmlReader reader, XmlTvProgram result) - { - /* - Gino's Italian Escape - Islands in the Sun: Southern Sardinia Celebrate the Sea - 8782 - */ - ProcessNode(reader, s => result.Episode.Title = s, _language); - } - - public void ProcessDescription(XmlReader reader, XmlTvProgram result) - { - ProcessNode(reader, s => result.Description = s, _language); - } - - public void ProcessTitleNode(XmlReader reader, XmlTvProgram result) - { - // Gino's Italian Escape - ProcessNode(reader, s => result.Title = s, _language); - } - - public void ProcessPremiereNode(XmlReader reader, XmlTvProgram result) - { - // Gino's Italian Escape - ProcessNode(reader, - s => - { - if (result.Premiere == null) - { - result.Premiere = new XmlTvPremiere() { Details = s }; - } - else - { - result.Premiere.Details = s; - } - }, _language); - } - - public XmlTvIcon ProcessIconNode(XmlReader reader) - { - var result = new XmlTvIcon(); - var isPopulated = false; - - var source = reader.GetAttribute("src"); - if (!string.IsNullOrEmpty(source)) - { - result.Source = source; - isPopulated = true; - } - - var widthString = reader.GetAttribute("width"); - var width = 0; - if (!string.IsNullOrEmpty(widthString) && int.TryParse(widthString, out width)) - { - result.Width = width; - isPopulated = true; - } - - var heightString = reader.GetAttribute("height"); - var height = 0; - if (!string.IsNullOrEmpty(heightString) && int.TryParse(heightString, out height)) - { - result.Height = height; - isPopulated = true; - } - - return isPopulated ? result : null; - } - - - //public void ProcessNodeWithLanguage(XmlReader reader, Action setter) - //{ - // var currentElementName = reader.Name; - // var result = string.Empty; - // var resultCandidate = reader.ReadElementContentAsString(); - // var exactMatchFound = false; - - // while (reader.Name == currentElementName) - // { - // var language = reader.GetAttribute("lang"); - // resultCandidate = reader.ReadElementContentAsString(); - - // if (language == _language && !exactMatchFound) - // { - // result = resultCandidate; - // } - - // reader.Skip(); - // } - - // result = String.IsNullOrEmpty(result) ? resultCandidate : result; - // setter(result); - //} - - public void ProcessNode(XmlReader reader, Action setter, string languageRequired = null, Action allOccurrencesSetter = null) - { - /* Homes Under the Hammer - Spanish - * Homes Under the Hammer - Spanish 2 - * Homes Under the Hammer - English - * Homes Under the Hammer - English 2 - * Homes Under the Hammer - Empty Language - * Homes Under the Hammer - Empty Language 2 - * Homes Under the Hammer - No Language - * Homes Under the Hammer - No Language 2 - */ - - /* Expected Behaviour: - * - Language = Null Homes Under the Hammer - No Language - * - Language = "" Homes Under the Hammer - No Language - * - Language = es Homes Under the Hammer - Spanish - * - Language = en Homes Under the Hammer - English - */ - - var results = new List>(); - - // We will always use the first value - so that if there are no matches we can return something - var currentElementName = reader.Name; - - var lang = reader.HasAttributes ? reader.GetAttribute("lang") : null; - var currentValue = reader.ReadElementContentAsString(); - results.Add(new Tuple(currentValue, lang)); - - if (allOccurrencesSetter != null) - { - allOccurrencesSetter(currentValue); - } - - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - if (reader.Name == currentElementName) - { - lang = reader.HasAttributes ? reader.GetAttribute("lang") : null; - currentValue = reader.ReadElementContentAsString(); - - if (allOccurrencesSetter != null) - { - allOccurrencesSetter(currentValue); - } - - results.Add(new Tuple(currentValue, lang)); - } - else - { - break; - } - } - else - { - reader.Read(); - } - } - - if (languageRequired != null) - { - foreach (var result in results) - { - if (string.Equals(languageRequired, result.Item2, StringComparison.OrdinalIgnoreCase)) - { - setter(result.Item1); - return; - } - } - } - - foreach (var result in results) - { - if (string.IsNullOrWhiteSpace(result.Item2)) - { - setter(result.Item1); - return; - } - } - - foreach (var result in results) - { - setter(result.Item1); - return; - } - } - - public void ProcessMultipleNodes(XmlReader reader, Action setter, string languageRequired = null) - { - /* Property - English - * Property - English 2 - * Property - Spanish - * Property - Spanish 2 - * Property - Empty Language - * Property - Empty Language 2 - * Property - No Language - * Property - No Language 2 - */ - - /* Expected Behaviour: - * - Language = Null Property - No Language / Property - No Language 2 - * - Language = "" Property - Empty Language / Property - Empty Language 2 - * - Language = es Property - Spanish / Property - Spanish 2 - * - Language = en Property - English / Property - English 2 - */ - - var currentElementName = reader.Name; - var values = new[] { new { Language = reader.GetAttribute("lang"), Value = reader.ReadElementContentAsString() } }.ToList(); - - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element && reader.Name != currentElementName) - { - break; - } - - if (reader.NodeType == XmlNodeType.Element && reader.Name == currentElementName) - { - values.Add(new { Language = reader.GetAttribute("lang"), Value = reader.ReadElementContentAsString() }); - } - } - - if (values.Count(v => v.Language == languageRequired) > 0) - { - values.RemoveAll(v => v.Language != languageRequired); - } - - // ENumerate and return all the matches - foreach (var result in values) - { - setter(result.Value); - } - } - - public void ProcessMultipleNodesWithLanguage(XmlReader reader, Action setter) - { - var currentElementName = reader.Name; - while (reader.Name == currentElementName) - { - var language = reader.GetAttribute("lang"); - if (string.IsNullOrEmpty(_language) || string.IsNullOrEmpty(language) || language == _language) - { - setter(reader.ReadElementContentAsString()); - } - reader.Skip(); - } - } - - private void PopulateHeader(XmlReader reader, XmlTvProgram result) - { - result.ChannelId = reader.GetAttribute("channel"); - - var startValue = reader.GetAttribute("start"); - if (string.IsNullOrEmpty(startValue)) - { - // Log.Error(" programme#{0} doesnt contain a start date", iChannel); - result.StartDate = DateTimeOffset.MinValue; - } - else - { - result.StartDate = ParseDate(startValue).GetValueOrDefault(); - } - - - var endValue = reader.GetAttribute("stop"); - if (string.IsNullOrEmpty(endValue)) - { - // Log.Error(" programme#{0} doesnt contain an end date", iChannel); - result.EndDate = DateTimeOffset.MinValue; - } - else - { - result.EndDate = ParseDate(endValue).GetValueOrDefault(); - } - } - - public const string _regDateWithOffset = @"^(?[0-9]{4,14})(\s(?[+-]*[0-9]{1,4}))?$"; - - public DateTimeOffset? ParseDate(string dateValue) - { - /* - All dates and times in this DTD follow the same format, loosely based - on ISO 8601. They can be 'YYYYMMDDhhmmss' or some initial - substring, for example if you only know the year and month you can - have 'YYYYMM'. You can also append a timezone to the end; if no - explicit timezone is given, UTC is assumed. Examples: - '200007281733 BST', '200209', '19880523083000 +0300'. (BST == +0100.) - */ - - if (string.IsNullOrEmpty(dateValue)) - { - return null; - } - - var completeDate = "20000101000000"; - var dateComponent = string.Empty; - var dateOffset = "+00:00"; - var match = Regex.Match(dateValue, _regDateWithOffset); - if (match.Success) - { - dateComponent = match.Groups["dateDigits"].Value; - if (!string.IsNullOrEmpty(match.Groups["dateOffset"].Value)) - { - dateOffset = match.Groups["dateOffset"].Value; // Add in the colon to ease parsing later - if (dateOffset.Length == 5) - { - dateOffset = dateOffset.Insert(3, ":"); // Add in the colon to ease parsing later - } - else - { - dateOffset = "+00:00"; - } - } - } - - // Pad out the date component part to 14 characaters so 2016061509 becomes 20160615090000 - if (dateComponent.Length < 14) - { - dateComponent = dateComponent + completeDate.Substring(dateComponent.Length, completeDate.Length - dateComponent.Length); - } - - var standardDate = string.Format("{0} {1}", dateComponent, dateOffset); - if (DateTimeOffset.TryParseExact(standardDate, "yyyyMMddHHmmss zzz", CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTimeOffset parsedDateTime)) - { - return parsedDateTime.ToUniversalTime(); - } - - // Logger.LogWarning("Unable to parse the date {0} from standardised form {1}", dateValue, standardDate); - - return null; - } - - public string StandardiseDate(string value) - { - var completeDate = "20000101000000"; - var dateComponent = string.Empty; - var dateOffset = "+0000"; - - var match = Regex.Match(value, _regDateWithOffset); - if (match.Success) - { - dateComponent = match.Groups["dateDigits"].Value; - dateOffset = match.Groups["dateOffset"].Value; - } - - // Pad out the date component part to 14 characaters so 2016061509 becomes 20160615090000 - if (dateComponent.Length < 14) - { - dateComponent = dateComponent + completeDate.Substring(dateComponent.Length, completeDate.Length - dateComponent.Length); - } - - return string.Format("{0} {1}", dateComponent, dateOffset); - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj b/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj deleted file mode 100644 index 04f5581736..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - netstandard2.0 - false - true - - - - - - - diff --git a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvChannel.cs b/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvChannel.cs deleted file mode 100644 index 2673d711c6..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvChannel.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Text; - -namespace Emby.XmlTv.Entities -{ - public class XmlTvChannel : IEquatable - { - public string Id { get; set; } - public string DisplayName { get; set; } - public string Number { get; set; } - public string Url { get; set; } - public XmlTvIcon Icon { get; set; } - - public bool Equals(XmlTvChannel other) - { - // If both are null, or both are same instance, return true. - if (ReferenceEquals(this, other)) - { - return true; - } - - // If the other is null then return false - if (other == null) - { - return false; - } - - // Return true if the fields match: - return Id == other.Id; - } - - public override int GetHashCode() - { - return (Id.GetHashCode() * 17); - } - - public override string ToString() - { - var builder = new StringBuilder(); - builder.AppendFormat("{0} - {1} ", Id, DisplayName); - - if (!string.IsNullOrEmpty(Url)) - { - builder.AppendFormat(" ({0})", Url); - } - - return builder.ToString(); - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvCredit.cs b/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvCredit.cs deleted file mode 100644 index d959ce76ff..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvCredit.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Emby.XmlTv.Entities -{ - public class XmlTvCredit - { - public XmlTvCreditType Type { get; set; } - public string Name { get; set; } - - public override string ToString() - { - return string.Format("{0} - ({1})", Name, Type); - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvCreditType.cs b/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvCreditType.cs deleted file mode 100644 index 31c7f5e440..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvCreditType.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Emby.XmlTv.Entities -{ - public enum XmlTvCreditType - { - NotSpecified = 0, - Director, - Actor, - Writer, - Adapter, - Producer, - Composer, - Editor, - Presenter, - Commentator, - Guest - } -} diff --git a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvEpisode.cs b/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvEpisode.cs deleted file mode 100644 index 47525b57ca..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvEpisode.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text; - -namespace Emby.XmlTv.Entities -{ - public class XmlTvEpisode - { - public int? Series { get; set; } - public int? SeriesCount { get; set; } - public int? Episode { get; set; } - public int? EpisodeCount { get; set; } - public string Title { get; set; } - public int? Part { get; set; } - public int? PartCount { get; set; } - - public override string ToString() - { - var builder = new StringBuilder(); - if (Series.HasValue || SeriesCount.HasValue) - { - builder.AppendFormat("Series {0}", Series.HasValue ? Series.Value.ToString() : "?"); - if (SeriesCount.HasValue) - { - builder.AppendFormat(" of {0}", SeriesCount); - } - } - - if (Episode.HasValue || EpisodeCount.HasValue) - { - builder.Append(builder.Length > 0 ? ", " : string.Empty); - builder.AppendFormat("Episode {0}", Episode.HasValue ? Episode.Value.ToString() : "?"); - if (EpisodeCount.HasValue) - { - builder.AppendFormat(" of {0}", EpisodeCount); - } - } - - if (Part.HasValue || PartCount.HasValue) - { - builder.Append(builder.Length > 0 ? ", " : string.Empty); - builder.AppendFormat("Part {0}", Part.HasValue ? Part.Value.ToString() : "?"); - if (PartCount.HasValue) - { - builder.AppendFormat(" of {0}", PartCount); - } - } - - return builder.ToString(); - } - } - - -} diff --git a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvIcon.cs b/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvIcon.cs deleted file mode 100644 index 77ef8d5fd0..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvIcon.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Text; - -namespace Emby.XmlTv.Entities -{ - public class XmlTvIcon - { - public string Source { get; set; } - public int? Width { get; set; } - public int? Height { get; set; } - - public override string ToString() - { - var builder = new StringBuilder(); - builder.AppendFormat("Source: {0}", Source); - if (Width.HasValue) - { - builder.AppendFormat(", Width: {0}", Width); - } - if (Height.HasValue) - { - builder.AppendFormat(", Height: {0}", Height); - } - - return builder.ToString(); - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvLanguage.cs b/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvLanguage.cs deleted file mode 100644 index 0fd5573cd4..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvLanguage.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Emby.XmlTv.Entities -{ - public class XmlTvLanguage - { - /// - /// The name. - /// - public string Name { get; set; } - - /// - /// The relevance (number of occurances) of the language, can be used to order (desc) - /// - public int Relevance { get; set; } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvPremiere.cs b/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvPremiere.cs deleted file mode 100644 index a1920bc779..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvPremiere.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Emby.XmlTv.Entities -{ - public class XmlTvPremiere - { - /* - - First showing on national terrestrial TV - - */ - - public string Details { get; set; } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvProgram.cs b/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvProgram.cs deleted file mode 100644 index 1725e17d70..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvProgram.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Emby.XmlTv.Entities -{ - public class XmlTvProgram : IEquatable - { - public string ChannelId { get; set; } - - public DateTimeOffset StartDate { get; set; } - - public DateTimeOffset EndDate { get; set; } - - public string Title { get; set; } - - public string Description { get; set; } - public string ProgramId { get; set; } - public string Quality { get; set; } - - public List Categories { get; set; } - - public List Countries { get; set; } - - public DateTimeOffset? PreviouslyShown { get; set; } - - public bool IsPreviouslyShown { get; set; } - public bool IsNew { get; set; } - - public DateTimeOffset? CopyrightDate { get; set; } - - public XmlTvEpisode Episode { get; set; } - - public List Credits { get; set; } - - public XmlTvRating Rating { get; set; } - - public float? StarRating { get; set; } - - public XmlTvIcon Icon { get; set; } - - public XmlTvPremiere Premiere { get; set; } - - public Dictionary ProviderIds { get; set; } - public Dictionary SeriesProviderIds { get; set; } - - public XmlTvProgram() - { - Credits = new List(); - Categories = new List(); - Countries = new List(); - Episode = new XmlTvEpisode(); - - ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - SeriesProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - public bool Equals(XmlTvProgram other) - { - // If both are null, or both are same instance, return true. - if (ReferenceEquals(this, other)) - { - return true; - } - - // If the other is null then return false - if (other == null) - { - return false; - } - - // Return true if the fields match: - return ChannelId == other.ChannelId && - StartDate == other.StartDate && - EndDate == other.EndDate; - } - - public override int GetHashCode() - { - return (ChannelId.GetHashCode() * 17) + (StartDate.GetHashCode() * 17) + (EndDate.GetHashCode() * 17); - } - - public override string ToString() - { - var builder = new StringBuilder(); - builder.AppendFormat("ChannelId: \t\t{0}\r\n", ChannelId); - builder.AppendFormat("Title: \t\t{0}\r\n", Title); - builder.AppendFormat("StartDate: \t\t{0}\r\n", StartDate); - builder.AppendFormat("EndDate: \t\t{0}\r\n", EndDate); - return builder.ToString(); - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvRating.cs b/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvRating.cs deleted file mode 100644 index e37113d7ab..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Entities/XmlTvRating.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Text; - -namespace Emby.XmlTv.Entities -{ - /// - /// Describes the rating (certification) applied to a program - /// - /// Example XML: - /// - public class XmlTvRating - { - /// - /// The literal name of the rating system - /// - /// MPAA - public string System { get; set; } - - /// - /// Describes the rating using the system specificed - /// - /// TV-14 - public string Value { get; set; } - - public override string ToString() - { - var builder = new StringBuilder(); - if (!string.IsNullOrEmpty(Value)) - { - builder.Append(Value); - } - - if (!string.IsNullOrEmpty(System)) - { - builder.AppendFormat(" ({0})", System); - } - - return builder.ToString(); - } - } -} diff --git a/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs b/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs deleted file mode 100644 index 7beec09cbd..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("XmlTv")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] diff --git a/Emby.XmlTv/Emby.XmlTv/XmlTvSchema.dtd b/Emby.XmlTv/Emby.XmlTv/XmlTvSchema.dtd deleted file mode 100644 index 889939224a..0000000000 --- a/Emby.XmlTv/Emby.XmlTv/XmlTvSchema.dtd +++ /dev/null @@ -1,575 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Emby.XmlTv/License.txt b/Emby.XmlTv/License.txt deleted file mode 100644 index 3c4f73ddb2..0000000000 --- a/Emby.XmlTv/License.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Alex Stevens - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Emby.XmlTv/Nuget/Emby.XmlTv.nuspec b/Emby.XmlTv/Nuget/Emby.XmlTv.nuspec deleted file mode 100644 index 087ce1e6ed..0000000000 --- a/Emby.XmlTv/Nuget/Emby.XmlTv.nuspec +++ /dev/null @@ -1,20 +0,0 @@ - - - - Emby.XmlTv - 1.0.19 - Emby.XmlTv - Emby Team - ebr,Luke,scottisafool - https://github.com/MediaBrowser/Emby.XmlTv - http://www.mb3admin.com/images/mb3icons1-1.png - false - An XmlTv parsing library. - Copyright © Emby 2013 - - - - - - - \ No newline at end of file diff --git a/Emby.XmlTv/README.md b/Emby.XmlTv/README.md deleted file mode 100644 index 86e777bb29..0000000000 --- a/Emby.XmlTv/README.md +++ /dev/null @@ -1 +0,0 @@ -# Emby.XmlTv diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 988ac364ae..febb1adabc 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -4,6 +4,7 @@ netstandard2.1 false true + true @@ -22,4 +23,16 @@ + + + + + + + + + + ../jellyfin.ruleset + + diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs index c72f295fdd..f2df066ec8 100644 --- a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs +++ b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs @@ -4,10 +4,19 @@ using SkiaSharp; namespace Jellyfin.Drawing.Skia { + /// + /// Static helper class used to draw percentage-played indicators on images. + /// public static class PercentPlayedDrawer { private const int IndicatorHeight = 8; + /// + /// Draw a percentage played indicator on a canvas. + /// + /// The canvas to draw the indicator on. + /// The size of the image being drawn on. + /// The percentage played to display with the indicator. public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent) { using (var paint = new SKPaint()) diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs index 7f3c18bb24..5084fd211c 100644 --- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs +++ b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs @@ -3,10 +3,21 @@ using SkiaSharp; namespace Jellyfin.Drawing.Skia { + /// + /// Static helper class for drawing 'played' indicators. + /// public static class PlayedIndicatorDrawer { private const int OffsetFromTopRightCorner = 38; + /// + /// Draw a 'played' indicator in the top right corner of a canvas. + /// + /// The canvas to draw the indicator on. + /// + /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the + /// indicator. + /// public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize) { var x = imageSize.Width - OffsetFromTopRightCorner; @@ -26,10 +37,10 @@ namespace Jellyfin.Drawing.Skia paint.TextSize = 30; paint.IsAntialias = true; + // or: + // var emojiChar = 0x1F680; var text = "✔️"; var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32); - // or: - //var emojiChar = 0x1F680; // ask the font manager for a font with that character var fontManager = SKFontManager.Default; diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/Jellyfin.Drawing.Skia/SkiaCodecException.cs index f848636bcb..8158b846dd 100644 --- a/Jellyfin.Drawing.Skia/SkiaCodecException.cs +++ b/Jellyfin.Drawing.Skia/SkiaCodecException.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Globalization; using SkiaSharp; @@ -8,16 +9,10 @@ namespace Jellyfin.Drawing.Skia /// public class SkiaCodecException : SkiaException { - /// - /// Returns the non-successfull codec result returned by Skia. - /// - /// The non-successfull codec result returned by Skia. - public SKCodecResult CodecResult { get; } - /// /// Initializes a new instance of the class. /// - /// The non-successfull codec result returned by Skia. + /// The non-successful codec result returned by Skia. public SkiaCodecException(SKCodecResult result) : base() { CodecResult = result; @@ -27,7 +22,7 @@ namespace Jellyfin.Drawing.Skia /// Initializes a new instance of the class /// with a specified error message. /// - /// The non-successfull codec result returned by Skia. + /// The non-successful codec result returned by Skia. /// The message that describes the error. public SkiaCodecException(SKCodecResult result, string message) : base(message) @@ -35,6 +30,11 @@ namespace Jellyfin.Drawing.Skia CodecResult = result; } + /// + /// Gets the non-successful codec result returned by Skia. + /// + public SKCodecResult CodecResult { get; } + /// public override string ToString() => string.Format( diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 66b814f6eb..b080b3e6a5 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -13,6 +13,9 @@ using static Jellyfin.Drawing.Skia.SkiaHelper; namespace Jellyfin.Drawing.Skia { + /// + /// Image encoder that uses to manipulate images. + /// public class SkiaEncoder : IImageEncoder { private readonly ILogger _logger; @@ -22,6 +25,12 @@ namespace Jellyfin.Drawing.Skia private static readonly HashSet _transparentImageTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; + /// + /// Initializes a new instance of the class. + /// + /// The application logger. + /// The application paths. + /// The application localization manager. public SkiaEncoder( ILogger logger, IApplicationPaths appPaths, @@ -32,12 +41,16 @@ namespace Jellyfin.Drawing.Skia _localizationManager = localizationManager; } + /// public string Name => "Skia"; + /// public bool SupportsImageCollageCreation => true; + /// public bool SupportsImageEncoding => true; + /// public IReadOnlyCollection SupportedInputFormats => new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -65,11 +78,12 @@ namespace Jellyfin.Drawing.Skia "arw" }; + /// public IReadOnlyCollection SupportedOutputFormats => new HashSet() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; /// - /// Test to determine if the native lib is available + /// Test to determine if the native lib is available. /// public static void TestSkia() { @@ -80,6 +94,11 @@ namespace Jellyfin.Drawing.Skia private static bool IsTransparent(SKColor color) => (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0; + /// + /// Convert a to a . + /// + /// The format to convert. + /// The converted format. public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat) { switch (selectedFormat) @@ -186,6 +205,9 @@ namespace Jellyfin.Drawing.Skia } /// + /// The path is null. + /// The path is not valid. + /// The file at the specified path could not be used to generate a codec. public ImageDimensions GetImageSize(string path) { if (path == null) @@ -269,6 +291,14 @@ namespace Jellyfin.Drawing.Skia } } + /// + /// Decode an image. + /// + /// The filepath of the image to decode. + /// Whether to force clean the bitmap. + /// The orientation of the image. + /// The detected origin of the image. + /// The resulting bitmap of the image. internal SKBitmap Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) { if (!File.Exists(path)) @@ -358,16 +388,6 @@ namespace Jellyfin.Drawing.Skia private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) { - //var transformations = { - // 2: { rotate: 0, flip: true}, - // 3: { rotate: 180, flip: false}, - // 4: { rotate: 180, flip: true}, - // 5: { rotate: 90, flip: true}, - // 6: { rotate: 90, flip: false}, - // 7: { rotate: 270, flip: true}, - // 8: { rotate: 270, flip: false}, - //} - switch (origin) { case SKEncodedOrigin.TopRight: @@ -497,6 +517,7 @@ namespace Jellyfin.Drawing.Skia } } + /// public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) { if (string.IsNullOrWhiteSpace(inputPath)) @@ -520,7 +541,7 @@ namespace Jellyfin.Drawing.Skia { if (bitmap == null) { - throw new ArgumentOutOfRangeException(string.Format("Skia unable to read image {0}", inputPath)); + throw new ArgumentOutOfRangeException($"Skia unable to read image {inputPath}"); } var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); @@ -556,7 +577,7 @@ namespace Jellyfin.Drawing.Skia } // create bitmap to use for canvas drawing used to draw into bitmap - using (var saveBitmap = new SKBitmap(width, height))//, bitmap.ColorType, bitmap.AlphaType)) + using (var saveBitmap = new SKBitmap(width, height)) // , bitmap.ColorType, bitmap.AlphaType)) using (var canvas = new SKCanvas(saveBitmap)) { // set background color if present @@ -609,9 +630,11 @@ namespace Jellyfin.Drawing.Skia } } } + return outputPath; } + /// public void CreateImageCollage(ImageCollageOptions options) { double ratio = (double)options.Width / options.Height; diff --git a/Jellyfin.Drawing.Skia/SkiaException.cs b/Jellyfin.Drawing.Skia/SkiaException.cs index 7aeaf083e2..968d3a2448 100644 --- a/Jellyfin.Drawing.Skia/SkiaException.cs +++ b/Jellyfin.Drawing.Skia/SkiaException.cs @@ -7,17 +7,30 @@ namespace Jellyfin.Drawing.Skia /// public class SkiaException : Exception { - /// + /// + /// Initializes a new instance of the class. + /// public SkiaException() : base() { } - /// + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. public SkiaException(string message) : base(message) { } - /// + /// + /// Initializes a new instance of the class with a specified error message and a + /// reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if + /// no inner exception is specified. + /// public SkiaException(string message, Exception innerException) : base(message, innerException) { diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 1f2a6e81a4..0735ef194a 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -5,15 +5,27 @@ using SkiaSharp; namespace Jellyfin.Drawing.Skia { + /// + /// Used to build collages of multiple images arranged in vertical strips. + /// public class StripCollageBuilder { private readonly SkiaEncoder _skiaEncoder; + /// + /// Initializes a new instance of the class. + /// + /// The encoder to use for building collages. public StripCollageBuilder(SkiaEncoder skiaEncoder) { _skiaEncoder = skiaEncoder; } + /// + /// Check which format an image has been encoded with using its filename extension. + /// + /// The path to the image to get the format for. + /// The image format. public static SKEncodedImageFormat GetEncodedFormat(string outputPath) { if (outputPath == null) @@ -48,6 +60,13 @@ namespace Jellyfin.Drawing.Skia return SKEncodedImageFormat.Png; } + /// + /// Create a square collage. + /// + /// The paths of the images to use in the collage. + /// The path at which to place the resulting collage image. + /// The desired width of the collage. + /// The desired height of the collage. public void BuildSquareCollage(string[] paths, string outputPath, int width, int height) { using (var bitmap = BuildSquareCollageBitmap(paths, width, height)) @@ -58,6 +77,13 @@ namespace Jellyfin.Drawing.Skia } } + /// + /// Create a thumb collage. + /// + /// The paths of the images to use in the collage. + /// The path at which to place the resulting image. + /// The desired width of the collage. + /// The desired height of the collage. public void BuildThumbCollage(string[] paths, string outputPath, int width, int height) { using (var bitmap = BuildThumbCollageBitmap(paths, width, height)) @@ -98,6 +124,7 @@ namespace Jellyfin.Drawing.Skia using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType)) { currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High); + // crop image int ix = (int)Math.Abs((iWidth - iSlice) / 2); using (var image = SKImage.FromBitmap(resizeBitmap)) diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs index dbf935f4e7..a10fff9dfe 100644 --- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs +++ b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs @@ -4,10 +4,25 @@ using SkiaSharp; namespace Jellyfin.Drawing.Skia { + /// + /// Static helper class for drawing unplayed count indicators. + /// public static class UnplayedCountIndicator { + /// + /// The x-offset used when drawing an unplayed count indicator. + /// private const int OffsetFromTopRightCorner = 38; + /// + /// Draw an unplayed count indicator in the top right corner of a canvas. + /// + /// The canvas to draw the indicator on. + /// + /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the + /// indicator. + /// + /// The number to draw in the indicator. public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count) { var x = imageSize.Width - OffsetFromTopRightCorner; @@ -19,6 +34,7 @@ namespace Jellyfin.Drawing.Skia paint.Style = SKPaintStyle.Fill; canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); } + using (var paint = new SKPaint()) { paint.Color = new SKColor(255, 255, 255, 255); @@ -33,6 +49,7 @@ namespace Jellyfin.Drawing.Skia { x -= 7; } + if (text.Length == 2) { x -= 13; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 5ac005b40b..712990a1ee 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Security; using System.Reflection; using System.Runtime.InteropServices; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -145,6 +146,10 @@ namespace Jellyfin.Server ApplicationHost.LogEnvironmentInfo(_logger, appPaths); + // Make sure we have all the code pages we can get + // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + // Increase the max http request limit // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); @@ -456,9 +461,9 @@ namespace Jellyfin.Server return new ConfigurationBuilder() .SetBasePath(appPaths.ConfigurationDirectoryPath) + .AddInMemoryCollection(ConfigurationOptions.Configuration) .AddJsonFile("logging.json", false, true) .AddEnvironmentVariables("JELLYFIN_") - .AddInMemoryCollection(ConfigurationOptions.Configuration) .Build(); } diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index b0333eb9ce..afc3e026a8 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -136,10 +136,11 @@ namespace MediaBrowser.Api /// System.Object. public object Get(GetPackage request) { - var packages = _installationManager.GetAvailablePackages().Result; - - var result = packages.FirstOrDefault(p => string.Equals(p.guid, request.AssemblyGuid ?? "none", StringComparison.OrdinalIgnoreCase)) - ?? packages.FirstOrDefault(p => p.name.Equals(request.Name, StringComparison.OrdinalIgnoreCase)); + var packages = _installationManager.GetAvailablePackages().GetAwaiter().GetResult(); + var result = _installationManager.FilterPackages( + packages, + request.Name, + string.IsNullOrEmpty(request.AssemblyGuid) ? default : Guid.Parse(request.AssemblyGuid)).FirstOrDefault(); return ToOptimizedResult(result); } @@ -184,7 +185,7 @@ namespace MediaBrowser.Api var package = _installationManager.GetCompatibleVersions( packages, request.Name, - new Guid(request.AssemblyGuid), + string.IsNullOrEmpty(request.AssemblyGuid) ? Guid.Empty : Guid.Parse(request.AssemblyGuid), string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version), request.UpdateClass).FirstOrDefault(); diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 1e9cd33136..5881e22a7c 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -63,8 +63,6 @@ namespace MediaBrowser.Api.Playback protected IDeviceManager DeviceManager { get; private set; } - protected ISubtitleEncoder SubtitleEncoder { get; private set; } - protected IMediaSourceManager MediaSourceManager { get; private set; } protected IJsonSerializer JsonSerializer { get; private set; } @@ -92,11 +90,11 @@ namespace MediaBrowser.Api.Playback IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) : base(logger, serverConfigurationManager, httpResultFactory) { UserManager = userManager; @@ -105,13 +103,12 @@ namespace MediaBrowser.Api.Playback MediaEncoder = mediaEncoder; FileSystem = fileSystem; DlnaManager = dlnaManager; - SubtitleEncoder = subtitleEncoder; DeviceManager = deviceManager; MediaSourceManager = mediaSourceManager; JsonSerializer = jsonSerializer; AuthorizationContext = authorizationContext; - EncodingHelper = new EncodingHelper(MediaEncoder, FileSystem, SubtitleEncoder); + EncodingHelper = encodingHelper; } /// @@ -148,8 +145,6 @@ namespace MediaBrowser.Api.Playback return Path.Combine(folder, filename + ext); } - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - protected virtual string GetDefaultEncoderPreset() { return "superfast"; @@ -764,13 +759,13 @@ namespace MediaBrowser.Api.Playback if (mediaSource == null) { - var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false)).ToList(); + var mediaSources = await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false); mediaSource = string.IsNullOrEmpty(request.MediaSourceId) ? mediaSources[0] : mediaSources.Find(i => string.Equals(i.Id, request.MediaSourceId)); - if (mediaSource == null && request.MediaSourceId.Equals(request.Id)) + if (mediaSource == null && Guid.Parse(request.MediaSourceId) == request.Id) { mediaSource = mediaSources[0]; } diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 8fdc6fa497..5d0dc98ddc 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -34,26 +34,26 @@ namespace MediaBrowser.Api.Playback.Hls IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) - : base( - logger, - serverConfigurationManager, - httpResultFactory, - userManager, - libraryManager, - isoManager, - mediaEncoder, - fileSystem, - dlnaManager, - subtitleEncoder, - deviceManager, - mediaSourceManager, - jsonSerializer, - authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) + : base( + logger, + serverConfigurationManager, + httpResultFactory, + userManager, + libraryManager, + isoManager, + mediaEncoder, + fileSystem, + dlnaManager, + deviceManager, + mediaSourceManager, + jsonSerializer, + authorizationContext, + encodingHelper) { } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index f09c7e9f2a..0178f53af6 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -104,12 +104,12 @@ namespace MediaBrowser.Api.Playback.Hls IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, - INetworkManager networkManager) + INetworkManager networkManager, + EncodingHelper encodingHelper) : base( logger, serverConfigurationManager, @@ -120,11 +120,11 @@ namespace MediaBrowser.Api.Playback.Hls mediaEncoder, fileSystem, dlnaManager, - subtitleEncoder, deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext) + authorizationContext, + encodingHelper) { NetworkManager = networkManager; } @@ -949,7 +949,17 @@ namespace MediaBrowser.Api.Playback.Hls var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()) + keyFrameArg; + args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()); + + // Unable to force key frames to h264_qsv transcode + if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + Logger.LogInformation("Bug Workaround: Disabling force_key_frames for h264_qsv"); + } + else + { + args += " " + keyFrameArg; + } //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 6d12a1ccd6..d1c53c1c11 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -27,6 +27,39 @@ namespace MediaBrowser.Api.Playback.Hls [Authenticated] public class VideoHlsService : BaseHlsService { + public VideoHlsService( + ILogger logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + IIsoManager isoManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + IDlnaManager dlnaManager, + IDeviceManager deviceManager, + IMediaSourceManager mediaSourceManager, + IJsonSerializer jsonSerializer, + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) + : base( + logger, + serverConfigurationManager, + httpResultFactory, + userManager, + libraryManager, + isoManager, + mediaEncoder, + fileSystem, + dlnaManager, + deviceManager, + mediaSourceManager, + jsonSerializer, + authorizationContext, + encodingHelper) + { + } + public Task Get(GetLiveHlsStream request) { return ProcessRequestAsync(request, true); @@ -136,38 +169,5 @@ namespace MediaBrowser.Api.Playback.Hls return args; } - - public VideoHlsService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - ILibraryManager libraryManager, - IIsoManager isoManager, - IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, - IDeviceManager deviceManager, - IMediaSourceManager mediaSourceManager, - IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) - : base( - logger, - serverConfigurationManager, - httpResultFactory, - userManager, - libraryManager, - isoManager, - mediaEncoder, - fileSystem, - dlnaManager, - subtitleEncoder, - deviceManager, - mediaSourceManager, - jsonSerializer, - authorizationContext) - { - } } } diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 11527007b5..8d1e3a3f23 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -43,11 +43,11 @@ namespace MediaBrowser.Api.Playback.Progressive IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) : base( logger, serverConfigurationManager, @@ -59,11 +59,11 @@ namespace MediaBrowser.Api.Playback.Progressive mediaEncoder, fileSystem, dlnaManager, - subtitleEncoder, deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext) + authorizationContext, + encodingHelper) { } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 4ada90d095..ed30dbba61 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -38,11 +38,11 @@ namespace MediaBrowser.Api.Playback.Progressive IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) : base( logger, serverConfigurationManager, @@ -53,11 +53,11 @@ namespace MediaBrowser.Api.Playback.Progressive mediaEncoder, fileSystem, dlnaManager, - subtitleEncoder, deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext) + authorizationContext, + encodingHelper) { HttpClient = httpClient; } diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 56ec86cd47..4de81655ce 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -80,11 +80,11 @@ namespace MediaBrowser.Api.Playback.Progressive IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) : base( logger, serverConfigurationManager, @@ -96,11 +96,11 @@ namespace MediaBrowser.Api.Playback.Progressive mediaEncoder, fileSystem, dlnaManager, - subtitleEncoder, deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext) + authorizationContext, + encodingHelper) { } diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs index b1450e2cc8..9cba9df139 100644 --- a/MediaBrowser.Api/Playback/UniversalAudioService.cs +++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs @@ -9,7 +9,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; @@ -75,6 +74,9 @@ namespace MediaBrowser.Api.Playback [Authenticated] public class UniversalAudioService : BaseApiService { + private readonly ILoggerFactory _loggerFactory; + private readonly EncodingHelper _encodingHelper; + public UniversalAudioService( ILogger logger, IServerConfigurationManager serverConfigurationManager, @@ -87,11 +89,11 @@ namespace MediaBrowser.Api.Playback IFileSystem fileSystem, IDlnaManager dlnaManager, IDeviceManager deviceManager, - ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, - INetworkManager networkManager) + INetworkManager networkManager, + EncodingHelper encodingHelper) : base(logger, serverConfigurationManager, httpResultFactory) { HttpClient = httpClient; @@ -102,11 +104,11 @@ namespace MediaBrowser.Api.Playback FileSystem = fileSystem; DlnaManager = dlnaManager; DeviceManager = deviceManager; - SubtitleEncoder = subtitleEncoder; MediaSourceManager = mediaSourceManager; JsonSerializer = jsonSerializer; AuthorizationContext = authorizationContext; NetworkManager = networkManager; + _encodingHelper = encodingHelper; } protected IHttpClient HttpClient { get; private set; } @@ -117,7 +119,6 @@ namespace MediaBrowser.Api.Playback protected IFileSystem FileSystem { get; private set; } protected IDlnaManager DlnaManager { get; private set; } protected IDeviceManager DeviceManager { get; private set; } - protected ISubtitleEncoder SubtitleEncoder { get; private set; } protected IMediaSourceManager MediaSourceManager { get; private set; } protected IJsonSerializer JsonSerializer { get; private set; } protected IAuthorizationContext AuthorizationContext { get; private set; } @@ -287,12 +288,12 @@ namespace MediaBrowser.Api.Playback MediaEncoder, FileSystem, DlnaManager, - SubtitleEncoder, DeviceManager, MediaSourceManager, JsonSerializer, AuthorizationContext, - NetworkManager) + NetworkManager, + _encodingHelper) { Request = Request }; @@ -337,11 +338,11 @@ namespace MediaBrowser.Api.Playback MediaEncoder, FileSystem, DlnaManager, - SubtitleEncoder, DeviceManager, MediaSourceManager, JsonSerializer, - AuthorizationContext) + AuthorizationContext, + _encodingHelper) { Request = Request }; diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index 81142b7289..c7b5051712 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -119,6 +119,7 @@ namespace MediaBrowser.Api.UserLibrary var result = new QueryResult { + StartIndex = request.StartIndex.GetValueOrDefault(), TotalRecordCount = itemsResult.TotalRecordCount, Items = returnItems }; @@ -169,6 +170,7 @@ namespace MediaBrowser.Api.UserLibrary return new QueryResult { + StartIndex = request.StartIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = dtoList }; @@ -229,7 +231,8 @@ namespace MediaBrowser.Api.UserLibrary return new QueryResult { Items = Array.Empty(), - TotalRecordCount = 0 + TotalRecordCount = 0, + StartIndex = 0 }; } @@ -242,7 +245,8 @@ namespace MediaBrowser.Api.UserLibrary return new QueryResult { Items = itemsArray, - TotalRecordCount = itemsArray.Length + TotalRecordCount = itemsArray.Length, + StartIndex = 0 }; } diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index a0f9ae46e4..88e67b6486 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -11,6 +11,7 @@ namespace MediaBrowser.Controller.Drawing /// /// The supported input formats. IReadOnlyCollection SupportedInputFormats { get; } + /// /// Gets the supported output formats. /// @@ -18,9 +19,9 @@ namespace MediaBrowser.Controller.Drawing IReadOnlyCollection SupportedOutputFormats { get; } /// - /// Gets the name. + /// Gets the display name for the encoder. /// - /// The name. + /// The display name. string Name { get; } /// @@ -35,17 +36,22 @@ namespace MediaBrowser.Controller.Drawing /// true if [supports image encoding]; otherwise, false. bool SupportsImageEncoding { get; } + /// + /// Get the dimensions of an image from the filesystem. + /// + /// The filepath of the image. + /// The image dimensions. ImageDimensions GetImageSize(string path); /// - /// Encodes the image. + /// Encode an image. /// string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); /// - /// Creates the image collage. + /// Create an image collage. /// - /// The options. + /// The options to use when creating the collage. void CreateImageCollage(ImageCollageOptions options); } } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 60906bdb08..af4d227bc8 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -137,7 +137,7 @@ namespace MediaBrowser.Controller.Entities /// The video3 D format. public Video3DFormat? Video3DFormat { get; set; } - public string[] GetPlayableStreamFileNames(IMediaEncoder mediaEncoder) + public string[] GetPlayableStreamFileNames() { var videoType = VideoType; @@ -153,7 +153,8 @@ namespace MediaBrowser.Controller.Entities { return Array.Empty(); } - return mediaEncoder.GetPlayableStreamFileNames(Path, videoType); + + throw new NotImplementedException(); } /// diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs new file mode 100644 index 0000000000..76c9b4b26c --- /dev/null +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Configuration; + +namespace MediaBrowser.Controller.Extensions +{ + /// + /// Configuration extensions for MediaBrowser.Controller. + /// + public static class ConfigurationExtensions + { + /// + /// The key for the FFmpeg probe size option. + /// + public const string FfmpegProbeSizeKey = "FFmpeg:probesize"; + + /// + /// The key for the FFmpeg analyse duration option. + /// + public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration"; + + /// + /// Retrieves the FFmpeg probe size from the . + /// + /// This configuration. + /// The FFmpeg probe size option. + public static string GetFFmpegProbeSize(this IConfiguration configuration) + => configuration[FfmpegProbeSizeKey]; + + /// + /// Retrieves the FFmpeg analyse duration from the . + /// + /// This configuration. + /// The FFmpeg analyse duration option. + public static string GetFFmpegAnalyzeDuration(this IConfiguration configuration) + => configuration[FfmpegAnalyzeDurationKey]; + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 276eb71bcf..60c76ef7db 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -7,6 +7,10 @@ https://github.com/jellyfin/jellyfin + + + + diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0664bdd984..020f0553ed 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6,12 +6,14 @@ using System.Linq; using System.Text; using System.Threading; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Configuration; namespace MediaBrowser.Controller.MediaEncoding { @@ -22,6 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfiguration _configuration; private static readonly string[] _videoProfiles = new[] { @@ -34,11 +37,16 @@ namespace MediaBrowser.Controller.MediaEncoding "ConstrainedHigh" }; - public EncodingHelper(IMediaEncoder mediaEncoder, IFileSystem fileSystem, ISubtitleEncoder subtitleEncoder) + public EncodingHelper( + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration) { _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; _subtitleEncoder = subtitleEncoder; + _configuration = configuration; } public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) @@ -172,7 +180,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - public string GetInputFormat(string container) + public static string GetInputFormat(string container) { if (string.IsNullOrEmpty(container)) { @@ -467,6 +475,27 @@ namespace MediaBrowser.Controller.MediaEncoding .Append(' '); } + if (state.IsVideoRequest + && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + { + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions); + var outputVideoCodec = GetVideoEncoder(state, encodingOptions); + + if (encodingOptions.EnableHardwareEncoding && outputVideoCodec.Contains("qsv", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(videoDecoder) && videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)) + { + arg.Append("-hwaccel qsv "); + } + else + { + arg.Append("-init_hw_device qsv=hw -filter_hw_device hw "); + } + } + + arg.Append(videoDecoder + " "); + } + arg.Append("-i ") .Append(GetInputPathArgument(state)); @@ -641,7 +670,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(state.SubtitleStream.Language)) { - var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language, state.MediaSource.Protocol, CancellationToken.None).Result; + var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet( + subtitlePath, + state.SubtitleStream.Language, + state.MediaSource.Protocol, + CancellationToken.None).GetAwaiter().GetResult(); if (!string.IsNullOrEmpty(charenc)) { @@ -1562,6 +1595,7 @@ namespace MediaBrowser.Controller.MediaEncoding var videoSizeParam = string.Empty; + // Setup subtitle scaling if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) { videoSizeParam = string.Format( @@ -1570,7 +1604,11 @@ namespace MediaBrowser.Controller.MediaEncoding state.VideoStream.Width.Value, state.VideoStream.Height.Value); - videoSizeParam += ":force_original_aspect_ratio=decrease"; + //For QSV, feed it into hardware encoder now + if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + videoSizeParam += ",hwupload=extra_hw_frames=64"; + } } var mapPrefix = state.SubtitleStream.IsExternal ? @@ -1581,9 +1619,31 @@ namespace MediaBrowser.Controller.MediaEncoding ? 0 : state.SubtitleStream.Index; + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); + + // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference) + var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\""; + + if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + /* + QSV in FFMpeg can now setup hardware overlay for transcodes. + For software decoding and hardware encoding option, frames must be hwuploaded into hardware + with fixed frame size. + */ + if (!string.IsNullOrEmpty(videoDecoder) && videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)) + { + retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; + } + else + { + retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; + } + } + return string.Format( CultureInfo.InvariantCulture, - " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"", + retStr, mapPrefix, subtitleStreamIndex, state.VideoStream.Index, @@ -1647,31 +1707,34 @@ namespace MediaBrowser.Controller.MediaEncoding requestedMaxWidth, requestedMaxHeight); - if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) && width.HasValue && height.HasValue) { - // Work around vaapi's reduced scaling features - var scaler = "scale_vaapi"; - // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions // (outputWidth, outputHeight). The user may request precise output dimensions or maximum // output dimensions. Output dimensions are guaranteed to be even. var outputWidth = width.Value; var outputHeight = height.Value; + var vaapi_or_qsv = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ? "qsv" : "vaapi"; if (!videoWidth.HasValue || outputWidth != videoWidth.Value || !videoHeight.HasValue || outputHeight != videoHeight.Value) { + // Force nv12 pixel format to enable 10-bit to 8-bit colour conversion. filters.Add( string.Format( CultureInfo.InvariantCulture, - "{0}=w={1}:h={2}", - scaler, + "scale_{0}=w={1}:h={2}:format=nv12", + vaapi_or_qsv, outputWidth, outputHeight)); + } + else + { + filters.Add(string.Format(CultureInfo.InvariantCulture, "scale_{0}=format=nv12", vaapi_or_qsv)); } } else if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 @@ -1897,7 +1960,7 @@ namespace MediaBrowser.Controller.MediaEncoding // If transcoding from 10 bit, transform colour spaces too if (!string.IsNullOrEmpty(videoStream.PixelFormat) && videoStream.PixelFormat.IndexOf("p10", StringComparison.OrdinalIgnoreCase) != -1 - && string.Equals(outputVideoCodec,"libx264", StringComparison.OrdinalIgnoreCase)) + && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) { filters.Add("format=p010le"); filters.Add("format=nv12"); @@ -1914,10 +1977,26 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add("hwupload"); } - if (state.DeInterlace("h264", true) - && string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); + + // If we are software decoding, and hardware encoding + if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + && (string.IsNullOrEmpty(videoDecoder) || !videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))) { - filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi")); + filters.Add("format=nv12|qsv"); + filters.Add("hwupload=extra_hw_frames=64"); + } + + if (state.DeInterlace("h264", true)) + { + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi")); + } + else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv")); + } } if ((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) @@ -1940,13 +2019,13 @@ namespace MediaBrowser.Controller.MediaEncoding var inputHeight = videoStream?.Height; var threeDFormat = state.MediaSource.Video3DFormat; - var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); - filters.AddRange(GetScalingFilters(inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); var output = string.Empty; - if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + if (state.SubtitleStream != null + && state.SubtitleStream.IsTextSubtitleStream + && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { var subParam = GetTextSubtitleParam(state); @@ -2035,11 +2114,11 @@ namespace MediaBrowser.Controller.MediaEncoding } } - public static string GetProbeSizeArgument(int numInputFiles) - => numInputFiles > 1 ? "-probesize 1G" : ""; + public string GetProbeSizeArgument(int numInputFiles) + => numInputFiles > 1 ? "-probesize " + _configuration.GetFFmpegProbeSize() : string.Empty; - public static string GetAnalyzeDurationArgument(int numInputFiles) - => numInputFiles > 1 ? "-analyzeduration 200M" : ""; + public string GetAnalyzeDurationArgument(int numInputFiles) + => numInputFiles > 1 ? "-analyzeduration " + _configuration.GetFFmpegAnalyzeDuration() : string.Empty; public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions) { @@ -2134,6 +2213,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions); + if (!string.IsNullOrEmpty(videoDecoder)) { inputModifier += " " + videoDecoder; diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index d032a849e7..37f0b11a74 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -15,6 +15,9 @@ namespace MediaBrowser.Controller.MediaEncoding /// public interface IMediaEncoder : ITranscoderSupport { + /// + /// The location of the discovered FFmpeg tool. + /// FFmpegLocation EncoderLocation { get; } /// @@ -97,7 +100,6 @@ namespace MediaBrowser.Controller.MediaEncoding void UpdateEncoderPath(string path, string pathType); bool SupportsEncoder(string encoder); - string[] GetPlayableStreamFileNames(string path, VideoType videoType); IEnumerable GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 04ff66991d..6bcd6cd46a 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; @@ -19,9 +19,9 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; namespace MediaBrowser.MediaEncoding.Encoder { @@ -31,55 +31,60 @@ namespace MediaBrowser.MediaEncoding.Encoder public class MediaEncoder : IMediaEncoder, IDisposable { /// - /// Gets the encoder path. + /// The default image extraction timeout in milliseconds. /// - /// The encoder path. - public string EncoderPath => FFmpegPath; - - /// - /// The location of the discovered FFmpeg tool. - /// - public FFmpegLocation EncoderLocation { get; private set; } + internal const int DefaultImageExtractionTimeout = 5000; private readonly ILogger _logger; - private readonly IJsonSerializer _jsonSerializer; - private string FFmpegPath; - private string FFprobePath; - protected readonly IServerConfigurationManager ConfigurationManager; - protected readonly IFileSystem FileSystem; - protected readonly Func SubtitleEncoder; - protected readonly Func MediaSourceManager; + private readonly IServerConfigurationManager _configurationManager; + private readonly IFileSystem _fileSystem; private readonly IProcessFactory _processFactory; - private readonly int DefaultImageExtractionTimeoutMs; - private readonly string StartupOptionFFmpegPath; + private readonly ILocalizationManager _localization; + private readonly Func _subtitleEncoder; + private readonly IConfiguration _configuration; + private readonly string _startupOptionFFmpegPath; private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); + + private readonly object _runningProcessesLock = new object(); private readonly List _runningProcesses = new List(); - private readonly ILocalizationManager _localization; + + private EncodingHelper _encodingHelper; + + private string _ffmpegPath; + private string _ffprobePath; public MediaEncoder( - ILoggerFactory loggerFactory, - IJsonSerializer jsonSerializer, - string startupOptionsFFmpegPath, + ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, - Func subtitleEncoder, - Func mediaSourceManager, IProcessFactory processFactory, - int defaultImageExtractionTimeoutMs, - ILocalizationManager localization) + ILocalizationManager localization, + Func subtitleEncoder, + IConfiguration configuration, + string startupOptionsFFmpegPath) { - _logger = loggerFactory.CreateLogger(nameof(MediaEncoder)); - _jsonSerializer = jsonSerializer; - StartupOptionFFmpegPath = startupOptionsFFmpegPath; - ConfigurationManager = configurationManager; - FileSystem = fileSystem; - SubtitleEncoder = subtitleEncoder; + _logger = logger; + _configurationManager = configurationManager; + _fileSystem = fileSystem; _processFactory = processFactory; - DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs; _localization = localization; + _startupOptionFFmpegPath = startupOptionsFFmpegPath; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; } + private EncodingHelper EncodingHelper + => LazyInitializer.EnsureInitialized( + ref _encodingHelper, + () => new EncodingHelper(this, _fileSystem, _subtitleEncoder(), _configuration)); + + /// + public string EncoderPath => _ffmpegPath; + + /// + public FFmpegLocation EncoderLocation { get; private set; } + /// /// Run at startup or if the user removes a Custom path from transcode page. /// Sets global variables FFmpegPath. @@ -88,39 +93,39 @@ namespace MediaBrowser.MediaEncoding.Encoder public void SetFFmpegPath() { // 1) Custom path stored in config/encoding xml file under tag takes precedence - if (!ValidatePath(ConfigurationManager.GetConfiguration("encoding").EncoderAppPath, FFmpegLocation.Custom)) + if (!ValidatePath(_configurationManager.GetConfiguration("encoding").EncoderAppPath, FFmpegLocation.Custom)) { // 2) Check if the --ffmpeg CLI switch has been given - if (!ValidatePath(StartupOptionFFmpegPath, FFmpegLocation.SetByArgument)) + if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument)) { // 3) Search system $PATH environment variable for valid FFmpeg if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System)) { EncoderLocation = FFmpegLocation.NotFound; - FFmpegPath = null; + _ffmpegPath = null; } } } // Write the FFmpeg path to the config/encoding.xml file as so it appears in UI - var config = ConfigurationManager.GetConfiguration("encoding"); - config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty; - ConfigurationManager.SaveConfiguration("encoding", config); + var config = _configurationManager.GetConfiguration("encoding"); + config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty; + _configurationManager.SaveConfiguration("encoding", config); // Only if mpeg path is set, try and set path to probe - if (FFmpegPath != null) + if (_ffmpegPath != null) { // Determine a probe path from the mpeg path - FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); + _ffprobePath = Regex.Replace(_ffmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); // Interrogate to understand what coders are supported - var validator = new EncoderValidator(_logger, FFmpegPath); + var validator = new EncoderValidator(_logger, _ffmpegPath); SetAvailableDecoders(validator.GetDecoders()); SetAvailableEncoders(validator.GetEncoders()); } - _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, FFmpegPath ?? string.Empty); + _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, _ffmpegPath ?? string.Empty); } /// @@ -160,9 +165,9 @@ namespace MediaBrowser.MediaEncoding.Encoder // Write the new ffmpeg path to the xml as // This ensures its not lost on next startup - var config = ConfigurationManager.GetConfiguration("encoding"); + var config = _configurationManager.GetConfiguration("encoding"); config.EncoderAppPath = newPath; - ConfigurationManager.SaveConfiguration("encoding", config); + _configurationManager.SaveConfiguration("encoding", config); // Trigger SetFFmpegPath so we validate the new path and setup probe path SetFFmpegPath(); @@ -193,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // ToDo - Enable the ffmpeg validator. At the moment any version can be used. rc = true; - FFmpegPath = path; + _ffmpegPath = path; EncoderLocation = location; } else @@ -209,7 +214,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { try { - var files = FileSystem.GetFilePaths(path); + var files = _fileSystem.GetFilePaths(path); var excludeExtensions = new[] { ".c" }; @@ -304,7 +309,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; - var inputFiles = MediaEncoderHelpers.GetInputArgument(FileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames); + var inputFiles = MediaEncoderHelpers.GetInputArgument(_fileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames); var probeSize = EncodingHelper.GetProbeSizeArgument(inputFiles.Length); string analyzeDuration; @@ -365,7 +370,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Must consume both or ffmpeg may hang due to deadlocks. See comments below. RedirectStandardOutput = true, - FileName = FFprobePath, + FileName = _ffprobePath, Arguments = args, @@ -383,7 +388,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); } - using (var processWrapper = new ProcessWrapper(process, this, _logger)) + using (var processWrapper = new ProcessWrapper(process, this)) { _logger.LogDebug("Starting ffprobe with args {Args}", args); StartProcess(processWrapper); @@ -391,7 +396,7 @@ namespace MediaBrowser.MediaEncoding.Encoder InternalMediaInfoResult result; try { - result = await _jsonSerializer.DeserializeFromStreamAsync( + result = await JsonSerializer.DeserializeAsync( process.StandardOutput.BaseStream).ConfigureAwait(false); } catch @@ -423,7 +428,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return new ProbeResultNormalizer(_logger, FileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); + return new ProbeResultNormalizer(_logger, _fileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); } } @@ -486,7 +491,7 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new ArgumentNullException(nameof(inputPath)); } - var tempExtractPath = Path.Combine(ConfigurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); + var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar then scale to width 600. @@ -545,7 +550,6 @@ namespace MediaBrowser.MediaEncoding.Encoder args = string.Format("-ss {0} ", GetTimeParameter(offset.Value)) + args; } - var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder()); if (videoStream != null) { /* fix @@ -559,7 +563,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrWhiteSpace(container)) { - var inputFormat = encodinghelper.GetInputFormat(container); + var inputFormat = EncodingHelper.GetInputFormat(container); if (!string.IsNullOrWhiteSpace(inputFormat)) { args = "-f " + inputFormat + " " + args; @@ -570,7 +574,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFmpegPath, + FileName = _ffmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, @@ -579,7 +583,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - using (var processWrapper = new ProcessWrapper(process, this, _logger)) + using (var processWrapper = new ProcessWrapper(process, this)) { bool ranToCompletion; @@ -588,10 +592,10 @@ namespace MediaBrowser.MediaEncoding.Encoder { StartProcess(processWrapper); - var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs; + var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs; if (timeoutMs <= 0) { - timeoutMs = DefaultImageExtractionTimeoutMs; + timeoutMs = DefaultImageExtractionTimeout; } ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false); @@ -607,7 +611,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; - var file = FileSystem.GetFileInfo(tempExtractPath); + var file = _fileSystem.GetFileInfo(tempExtractPath); if (exitCode == -1 || !file.Exists || file.Length == 0) { @@ -675,7 +679,6 @@ namespace MediaBrowser.MediaEncoding.Encoder args = analyzeDurationArgument + " " + args; } - var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder()); if (videoStream != null) { /* fix @@ -689,7 +692,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrWhiteSpace(container)) { - var inputFormat = encodinghelper.GetInputFormat(container); + var inputFormat = EncodingHelper.GetInputFormat(container); if (!string.IsNullOrWhiteSpace(inputFormat)) { args = "-f " + inputFormat + " " + args; @@ -700,7 +703,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFmpegPath, + FileName = _ffmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, @@ -713,7 +716,7 @@ namespace MediaBrowser.MediaEncoding.Encoder bool ranToCompletion = false; - using (var processWrapper = new ProcessWrapper(process, this, _logger)) + using (var processWrapper = new ProcessWrapper(process, this)) { try { @@ -736,10 +739,10 @@ namespace MediaBrowser.MediaEncoding.Encoder cancellationToken.ThrowIfCancellationRequested(); - var jpegCount = FileSystem.GetFilePaths(targetDirectory) + var jpegCount = _fileSystem.GetFilePaths(targetDirectory) .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase)); - isResponsive = (jpegCount > lastCount); + isResponsive = jpegCount > lastCount; lastCount = jpegCount; } @@ -770,7 +773,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { process.Process.Start(); - lock (_runningProcesses) + lock (_runningProcessesLock) { _runningProcesses.Add(process); } @@ -804,7 +807,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private void StopProcesses() { List proceses; - lock (_runningProcesses) + lock (_runningProcessesLock) { proceses = _runningProcesses.ToList(); _runningProcesses.Clear(); @@ -827,12 +830,11 @@ namespace MediaBrowser.MediaEncoding.Encoder return path.Replace('\\', '/').Replace(":", "\\:").Replace("'", "'\\\\\\''"); } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// + /// public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// @@ -852,11 +854,6 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new NotImplementedException(); } - public string[] GetPlayableStreamFileNames(string path, VideoType videoType) - { - throw new NotImplementedException(); - } - public IEnumerable GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber) { throw new NotImplementedException(); @@ -870,21 +867,24 @@ namespace MediaBrowser.MediaEncoding.Encoder private class ProcessWrapper : IDisposable { - public readonly IProcess Process; - public bool HasExited; - public int? ExitCode; private readonly MediaEncoder _mediaEncoder; - private readonly ILogger _logger; - public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder, ILogger logger) + private bool _disposed = false; + + public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder) { Process = process; _mediaEncoder = mediaEncoder; - _logger = logger; - Process.Exited += Process_Exited; + Process.Exited += OnProcessExited; } - void Process_Exited(object sender, EventArgs e) + public IProcess Process { get; } + + public bool HasExited { get; private set; } + + public int? ExitCode { get; private set; } + + void OnProcessExited(object sender, EventArgs e) { var process = (IProcess)sender; @@ -903,7 +903,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private void DisposeProcess(IProcess process) { - lock (_mediaEncoder._runningProcesses) + lock (_mediaEncoder._runningProcessesLock) { _mediaEncoder._runningProcesses.Remove(this); } @@ -917,23 +917,18 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - private bool _disposed; - private readonly object _syncLock = new object(); public void Dispose() { - lock (_syncLock) + if (!_disposed) { - if (!_disposed) + if (Process != null) { - if (Process != null) - { - Process.Exited -= Process_Exited; - DisposeProcess(Process); - } + Process.Exited -= OnProcessExited; + DisposeProcess(Process); } - - _disposed = true; } + + _disposed = true; } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs index 3401c2d670..dec714121d 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles { /// - /// Interface ISubtitleWriter + /// Interface ISubtitleWriter. /// public interface ISubtitleWriter { diff --git a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs index 8995fcfe1f..241ebc6df5 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs @@ -1,27 +1,39 @@ using System.IO; -using System.Text; +using System.Text.Json; using System.Threading; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.MediaEncoding.Subtitles { + /// + /// JSON subtitle writer. + /// public class JsonWriter : ISubtitleWriter { - private readonly IJsonSerializer _json; - - public JsonWriter(IJsonSerializer json) - { - _json = json; - } - + /// public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { - using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + using (var writer = new Utf8JsonWriter(stream)) { - var json = _json.SerializeToString(info); + var trackevents = info.TrackEvents; + writer.WriteStartArray("TrackEvents"); - writer.Write(json); + for (int i = 0; i < trackevents.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var current = trackevents[i]; + writer.WriteStartObject(); + + writer.WriteString("Id", current.Id); + writer.WriteString("Text", current.Text); + writer.WriteNumber("StartPositionTicks", current.StartPositionTicks); + writer.WriteNumber("EndPositionTicks", current.EndPositionTicks); + + writer.WriteEndObject(); + } + + writer.WriteEndObject(); } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs index 6f96a641e9..45b317b2ed 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs @@ -14,14 +14,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { - var index = 1; + var trackEvents = info.TrackEvents; - foreach (var trackEvent in info.TrackEvents) + for (int i = 0; i < trackEvents.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); - writer.WriteLine(index.ToString(CultureInfo.InvariantCulture)); - writer.WriteLine(@"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}", TimeSpan.FromTicks(trackEvent.StartPositionTicks), TimeSpan.FromTicks(trackEvent.EndPositionTicks)); + var trackEvent = trackEvents[i]; + + writer.WriteLine((i + 1).ToString(CultureInfo.InvariantCulture)); + writer.WriteLine( + @"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}", + TimeSpan.FromTicks(trackEvent.StartPositionTicks), + TimeSpan.FromTicks(trackEvent.EndPositionTicks)); var text = trackEvent.Text; @@ -29,9 +34,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); writer.WriteLine(text); - writer.WriteLine(string.Empty); - - index++; + writer.WriteLine(); } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index d5fa76c3ab..183d7566d4 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -17,7 +17,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; using UtfUnknown; @@ -30,28 +29,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; - private readonly IJsonSerializer _json; private readonly IHttpClient _httpClient; private readonly IMediaSourceManager _mediaSourceManager; private readonly IProcessFactory _processFactory; public SubtitleEncoder( ILibraryManager libraryManager, - ILoggerFactory loggerFactory, + ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, - IJsonSerializer json, IHttpClient httpClient, IMediaSourceManager mediaSourceManager, IProcessFactory processFactory) { _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(nameof(SubtitleEncoder)); + _logger = logger; _appPaths = appPaths; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; - _json = json; _httpClient = httpClient; _mediaSourceManager = mediaSourceManager; _processFactory = processFactory; @@ -59,7 +55,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); - private Stream ConvertSubtitles(Stream stream, + private Stream ConvertSubtitles( + Stream stream, string inputFormat, string outputFormat, long startTimeTicks, @@ -170,7 +167,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles && (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd)) { var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSource.Id)); - inputFiles = mediaSourceItem.GetPlayableStreamFileNames(_mediaEncoder); + inputFiles = mediaSourceItem.GetPlayableStreamFileNames(); } else { @@ -179,32 +176,27 @@ namespace MediaBrowser.MediaEncoding.Subtitles var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false); - var stream = await GetSubtitleStream(fileInfo.Path, subtitleStream.Language, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); + var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); return (stream, fileInfo.Format); } - private async Task GetSubtitleStream(string path, string language, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken) + private async Task GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken) { if (requiresCharset) { - var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false); - - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; - _logger.LogDebug("charset {CharSet} detected for {Path}", charset ?? "null", path); - - if (!string.IsNullOrEmpty(charset)) + using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) { - // Make sure we have all the code pages we can get - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - using (var inputStream = new MemoryStream(bytes)) - using (var reader = new StreamReader(inputStream, Encoding.GetEncoding(charset))) + var result = CharsetDetector.DetectFromStream(stream).Detected; + + if (result != null) { + _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, path); + + using var reader = new StreamReader(stream, result.Encoding); var text = await reader.ReadToEndAsync().ConfigureAwait(false); - bytes = Encoding.UTF8.GetBytes(text); - - return new MemoryStream(bytes); + return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } } @@ -323,7 +315,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) { - return new JsonWriter(_json); + return new JsonWriter(); } if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) { @@ -544,7 +536,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!File.Exists(outputPath)) { - await ExtractTextSubtitleInternal(_mediaEncoder.GetInputArgument(inputFiles, protocol), subtitleStreamIndex, outputCodec, outputPath, cancellationToken).ConfigureAwait(false); + await ExtractTextSubtitleInternal( + _mediaEncoder.GetInputArgument(inputFiles, protocol), + subtitleStreamIndex, + outputCodec, + outputPath, + cancellationToken).ConfigureAwait(false); } } finally @@ -572,8 +569,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath, - subtitleStreamIndex, outputCodec, outputPath); + var processArgs = string.Format( + CultureInfo.InvariantCulture, + "-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", + inputPath, + subtitleStreamIndex, + outputCodec, + outputPath); var process = _processFactory.Create(new ProcessOptions { @@ -721,41 +723,38 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } + /// public async Task GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken) { - var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false); + using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) + { + var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName; - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; + _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path); - _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path); - - return charset; + return charset; + } } - private async Task GetBytes(string path, MediaProtocol protocol, CancellationToken cancellationToken) + private Task GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) { - if (protocol == MediaProtocol.Http) + switch (protocol) { - var opts = new HttpRequestOptions() - { - Url = path, - CancellationToken = cancellationToken - }; - using (var file = await _httpClient.Get(opts).ConfigureAwait(false)) - using (var memoryStream = new MemoryStream()) - { - await file.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; + case MediaProtocol.Http: + var opts = new HttpRequestOptions() + { + Url = path, + CancellationToken = cancellationToken, + BufferContent = true + }; - return memoryStream.ToArray(); - } - } - if (protocol == MediaProtocol.File) - { - return File.ReadAllBytes(path); - } + return _httpClient.Get(opts); - throw new ArgumentOutOfRangeException(nameof(protocol)); + case MediaProtocol.File: + return Task.FromResult(File.OpenRead(path)); + default: + throw new ArgumentOutOfRangeException(nameof(protocol)); + } } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs index cdaf949641..4f15bac496 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs @@ -49,12 +49,5 @@ namespace MediaBrowser.MediaEncoding.Subtitles writer.WriteLine(""); } } - - private string FormatTime(long ticks) - { - var time = TimeSpan.FromTicks(ticks); - - return string.Format(@"{0:hh\:mm\:ss\,fff}", time); - } } } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 9de6fef619..cf6d9c2f6c 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -233,7 +233,6 @@ namespace MediaBrowser.Model.Configuration LocalNetworkSubnets = Array.Empty(); LocalNetworkAddresses = Array.Empty(); CodecsUsed = Array.Empty(); - ImageExtractionTimeoutMs = 0; PathSubstitutions = Array.Empty(); IgnoreVirtualInterfaces = false; EnableSimpleArtistDetection = true; diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs index 962f4d2fe3..c382b20c9a 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs @@ -1,12 +1,15 @@ +using System; +using System.Collections.Generic; + namespace MediaBrowser.Model.MediaInfo { public class SubtitleTrackInfo { - public SubtitleTrackEvent[] TrackEvents { get; set; } + public IReadOnlyList TrackEvents { get; set; } public SubtitleTrackInfo() { - TrackEvents = new SubtitleTrackEvent[] { }; + TrackEvents = Array.Empty(); } } } diff --git a/MediaBrowser.Model/Querying/QueryResult.cs b/MediaBrowser.Model/Querying/QueryResult.cs index c007a45d6e..221645afbf 100644 --- a/MediaBrowser.Model/Querying/QueryResult.cs +++ b/MediaBrowser.Model/Querying/QueryResult.cs @@ -17,6 +17,12 @@ namespace MediaBrowser.Model.Querying /// The total record count. public int TotalRecordCount { get; set; } + /// + /// The index of the first record in Items. + /// + /// First record index. + public int StartIndex { get; set; } + public QueryResult() { Items = Array.Empty(); diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index e0b23108f0..95b915b3d8 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -62,7 +62,11 @@ namespace MediaBrowser.Providers.MediaInfo { var protocol = item.PathProtocol ?? MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, item.Path, null, item.GetPlayableStreamFileNames(_mediaEncoder)); + var inputPath = MediaEncoderHelpers.GetInputArgument( + _fileSystem, + item.Path, + null, + item.GetPlayableStreamFileNames()); var mediaStreams = item.GetMediaStreams(); diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs index e5287048dc..4269d34201 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs @@ -35,9 +35,8 @@ namespace MediaBrowser.Providers.TV.TheTVDB { var list = new List(); - // The search query must either provide an episode number or date - if (!searchInfo.IndexNumber.HasValue - || !searchInfo.PremiereDate.HasValue + // Either an episode number or date must be provided; and the dictionary of provider ids must be valid + if ((searchInfo.IndexNumber == null && searchInfo.PremiereDate == null) || !TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) { return list; diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs index 10ed4f0731..72ceadaf1d 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs @@ -170,11 +170,16 @@ namespace MediaBrowser.Providers.TV.TheTVDB return result?.Data.First().Id.ToString(); } + /// + /// Check whether a dictionary of provider IDs includes an entry for a valid TV metadata provider. + /// + /// The dictionary to check. + /// True, if the dictionary contains a valid TV provider ID, otherwise false. internal static bool IsValidSeries(Dictionary seriesProviderIds) { - return seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out _) || - seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out _) || - seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out _); + return seriesProviderIds.ContainsKey(MetadataProviders.Tvdb.ToString()) || + seriesProviderIds.ContainsKey(MetadataProviders.Imdb.ToString()) || + seriesProviderIds.ContainsKey(MetadataProviders.Zap2It.ToString()); } /// diff --git a/MediaBrowser.sln b/MediaBrowser.sln index da828927b3..cf3f56cd90 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -36,8 +36,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Notifications", "Emby. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.XmlTv", "Emby.XmlTv\Emby.XmlTv\Emby.XmlTv.csproj", "{6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" @@ -136,10 +134,6 @@ Global {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.Build.0 = Release|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.Build.0 = Release|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/deployment/ubuntu-package-x64/Dockerfile b/deployment/ubuntu-package-x64/Dockerfile index 99022891bd..d881c04e35 100644 --- a/deployment/ubuntu-package-x64/Dockerfile +++ b/deployment/ubuntu-package-x64/Dockerfile @@ -1,19 +1,28 @@ -FROM microsoft/dotnet:2.2-sdk-bionic +FROM ubuntu:bionic # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-x64 ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=3.0 # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 # Prepare Ubuntu build environment RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0 \ && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ && mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f51cfd8-311d-43fe-a887-c80b40358cfd/440d10dc2091b8d0f1a12b7124034e49/dotnet-sdk-3.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + # Install npm package manager RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ && echo "deb https://deb.nodesource.com/node_8.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \