Merge remote-tracking branch 'upstream/master' into bye-tvdb

pull/4298/head
crobibero 4 years ago
commit 7ff212576e

@ -62,6 +62,7 @@ jobs:
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: 'Download Reference Assembly Build Artifact' displayName: 'Download Reference Assembly Build Artifact'
enabled: false
inputs: inputs:
source: "specific" source: "specific"
artifact: "$(NugetPackageName)" artifact: "$(NugetPackageName)"
@ -73,6 +74,7 @@ jobs:
- task: CopyFiles@2 - task: CopyFiles@2
displayName: 'Copy Reference Assembly Build Artifact' displayName: 'Copy Reference Assembly Build Artifact'
enabled: false
inputs: inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: '**/*.dll' contents: '**/*.dll'
@ -83,6 +85,7 @@ jobs:
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool' displayName: 'Execute ABI Compatibility Check Tool'
enabled: false
inputs: inputs:
command: custom command: custom
custom: compat custom: compat

@ -28,30 +28,27 @@ jobs:
inputs: inputs:
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
# Generate npm api client ## Authenticate with npm registry
# Unstable - task: npmAuthenticate@0
- task: CmdLine@2
displayName: 'Build unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs: inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)" workingFile: ./.npmrc
customEndpoint: 'jellyfin-bot for NPM'
- task: Npm@1
displayName: 'Publish unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: publish
publishRegistry: useFeed
publishFeed: 'unstable@Local'
workingDir: ./apiclient/generated/typescript/axios
# Stable ## Generate npm api client
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Build stable typescript axios client' displayName: 'Build stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs: inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)" script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
## Run npm install
- task: Npm@1
displayName: 'Install npm dependencies'
inputs:
command: install
workingDir: ./apiclient/generated/typescript/axios
## Publish npm packages
- task: Npm@1 - task: Npm@1
displayName: 'Publish stable typescript axios client' displayName: 'Publish stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')

@ -63,6 +63,7 @@ jobs:
sshEndpoint: repository sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist' sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**' contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: OpenAPISpec - job: OpenAPISpec
dependsOn: Test dependsOn: Test
@ -166,7 +167,7 @@ jobs:
inputs: inputs:
sshEndpoint: repository sshEndpoint: repository
runOptions: 'commands' runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable & commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
- task: SSH@0 - task: SSH@0
displayName: 'Update Stable Repository' displayName: 'Update Stable Repository'
@ -175,7 +176,7 @@ jobs:
inputs: inputs:
sshEndpoint: repository sshEndpoint: repository
runOptions: 'commands' runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) & commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget - job: PublishNuget
displayName: 'Publish NuGet packages' displayName: 'Publish NuGet packages'

@ -0,0 +1,3 @@
registry=https://registry.npmjs.org/
@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
always-auth=true

@ -487,7 +487,7 @@ namespace Emby.Dlna.ContentDirectory
User = user, User = user,
Recursive = true, Recursive = true,
IsMissing = false, IsMissing = false,
ExcludeItemTypes = new[] { typeof(Book).Name }, ExcludeItemTypes = new[] { nameof(Book) },
IsFolder = isFolder, IsFolder = isFolder,
MediaTypes = mediaTypes, MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
@ -556,7 +556,7 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
IsVirtualItem = false, IsVirtualItem = false,
ExcludeItemTypes = new[] { typeof(Book).Name }, ExcludeItemTypes = new[] { nameof(Book) },
IsPlaceHolder = false, IsPlaceHolder = false,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
}; };
@ -575,7 +575,7 @@ namespace Emby.Dlna.ContentDirectory
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
}; };
query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }; query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
SetSorting(query, sort, false); SetSorting(query, sort, false);
@ -910,7 +910,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(Series).Name }; query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -923,7 +923,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(Movie).Name }; query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -936,7 +936,7 @@ namespace Emby.Dlna.ContentDirectory
// query.Parent = parent; // query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(BoxSet).Name }; query.IncludeItemTypes = new[] { nameof(BoxSet) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name }; query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -962,7 +962,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(Audio).Name }; query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -975,7 +975,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Audio).Name }; query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -988,7 +988,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Series).Name }; query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -1001,7 +1001,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Episode).Name }; query.IncludeItemTypes = new[] { nameof(Episode) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -1014,7 +1014,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Movie).Name }; query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -1027,7 +1027,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name }; query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -1181,7 +1181,7 @@ namespace Emby.Dlna.ContentDirectory
{ {
UserId = user.Id, UserId = user.Id,
Limit = 50, Limit = 50,
IncludeItemTypes = new[] { typeof(Episode).Name }, IncludeItemTypes = new[] { nameof(Episode) },
ParentId = parent == null ? Guid.Empty : parent.Id, ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false GroupItems = false
}, },
@ -1215,7 +1215,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true, Recursive = true,
ParentId = parentId, ParentId = parentId,
ArtistIds = new[] { item.Id }, ArtistIds = new[] { item.Id },
IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
@ -1259,7 +1259,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true, Recursive = true,
ParentId = parentId, ParentId = parentId,
GenreIds = new[] { item.Id }, GenreIds = new[] { item.Id },
IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
@ -1346,8 +1346,8 @@ namespace Emby.Dlna.ContentDirectory
{ {
if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase)) if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
{ {
stubType = (StubType)Enum.Parse(typeof(StubType), name, true); stubType = Enum.Parse<StubType>(name, true);
id = id.Split(new[] { '_' }, 2)[1]; id = id.Split('_', 2)[1];
break; break;
} }

@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
{ {
foreach (var att in profile.XmlRootAttributes) foreach (var att in profile.XmlRootAttributes)
{ {
var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2) if (parts.Length == 2)
{ {
writer.WriteAttributeString(parts[0], parts[1], null, att.Value); writer.WriteAttributeString(parts[0], parts[1], null, att.Value);

@ -383,9 +383,9 @@ namespace Emby.Dlna
continue; continue;
} }
var filename = Path.GetFileName(name).Substring(namespaceName.Length); var path = Path.Join(
systemProfilesPath,
var path = Path.Combine(systemProfilesPath, filename); Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
using (var stream = _assembly.GetManifestResourceStream(name)) using (var stream = _assembly.GetManifestResourceStream(name))
{ {

@ -168,7 +168,7 @@ namespace Emby.Dlna.Eventing
builder.Append("</e:propertyset>"); builder.Append("</e:propertyset>");
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl); using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml); options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType); options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange"); options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");

@ -257,9 +257,10 @@ namespace Emby.Dlna.Main
private async Task RegisterServerEndpoints() private async Task RegisterServerEndpoints()
{ {
var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false); var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
var udn = CreateUuid(_appHost.SystemId); var udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml";
foreach (var address in addresses) foreach (var address in addresses)
{ {
@ -279,7 +280,6 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var descriptorUri = "/dlna/" + udn + "/description.xml";
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
var device = new SsdpRootDevice var device = new SsdpRootDevice

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Xml; using System.Xml;
@ -10,8 +8,16 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {
/// <summary>
/// Defines the <see cref="ControlHandler" />.
/// </summary>
public class ControlHandler : BaseControlHandler public class ControlHandler : BaseControlHandler
{ {
/// <summary>
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
/// </summary>
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
public ControlHandler(IServerConfigurationManager config, ILogger logger) public ControlHandler(IServerConfigurationManager config, ILogger logger)
: base(config, logger) : base(config, logger)
{ {
@ -35,9 +41,17 @@ namespace Emby.Dlna.MediaReceiverRegistrar
throw new ResourceNotFoundException("Unexpected control request name: " + methodName); throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
} }
/// <summary>
/// Records that the handle is authorized in the xml stream.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private static void HandleIsAuthorized(XmlWriter xmlWriter) private static void HandleIsAuthorized(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1"); => xmlWriter.WriteElementString("Result", "1");
/// <summary>
/// Records that the handle is validated in the xml stream.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private static void HandleIsValidated(XmlWriter xmlWriter) private static void HandleIsValidated(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1"); => xmlWriter.WriteElementString("Result", "1");
} }

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.Service; using Emby.Dlna.Service;
@ -8,10 +6,19 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {
/// <summary>
/// Defines the <see cref="MediaReceiverRegistrarService" />.
/// </summary>
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
{ {
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
/// <summary>
/// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
public MediaReceiverRegistrarService( public MediaReceiverRegistrarService(
ILogger<MediaReceiverRegistrarService> logger, ILogger<MediaReceiverRegistrarService> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
@ -24,7 +31,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
/// <inheritdoc /> /// <inheritdoc />
public string GetServiceXml() public string GetServiceXml()
{ {
return new MediaReceiverRegistrarXmlBuilder().GetXml(); return MediaReceiverRegistrarXmlBuilder.GetXml();
} }
/// <inheritdoc /> /// <inheritdoc />

@ -1,79 +1,89 @@
#pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Service; using Emby.Dlna.Service;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {
public class MediaReceiverRegistrarXmlBuilder /// <summary>
/// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
/// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
/// </summary>
public static class MediaReceiverRegistrarXmlBuilder
{ {
public string GetXml() /// <summary>
/// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
/// </summary>
/// <returns>An XML representation of this service.</returns>
public static string GetXml()
{ {
return new ServiceXmlBuilder().GetXml( return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
new ServiceActionListBuilder().GetActions(),
GetStateVariables());
} }
/// <summary>
/// The a list of all the state variables for this invocation.
/// </summary>
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables() private static IEnumerable<StateVariable> GetStateVariables()
{ {
var list = new List<StateVariable>(); var list = new List<StateVariable>
list.Add(new StateVariable
{ {
Name = "AuthorizationGrantedUpdateID", new StateVariable
DataType = "ui4", {
SendsEvents = true Name = "AuthorizationGrantedUpdateID",
}); DataType = "ui4",
SendsEvents = true
},
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_DeviceID", Name = "A_ARG_TYPE_DeviceID",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "AuthorizationDeniedUpdateID", Name = "AuthorizationDeniedUpdateID",
DataType = "ui4", DataType = "ui4",
SendsEvents = true SendsEvents = true
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "ValidationSucceededUpdateID", Name = "ValidationSucceededUpdateID",
DataType = "ui4", DataType = "ui4",
SendsEvents = true SendsEvents = true
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_RegistrationRespMsg", Name = "A_ARG_TYPE_RegistrationRespMsg",
DataType = "bin.base64", DataType = "bin.base64",
SendsEvents = false SendsEvents = false
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_RegistrationReqMsg", Name = "A_ARG_TYPE_RegistrationReqMsg",
DataType = "bin.base64", DataType = "bin.base64",
SendsEvents = false SendsEvents = false
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "ValidationRevokedUpdateID", Name = "ValidationRevokedUpdateID",
DataType = "ui4", DataType = "ui4",
SendsEvents = true SendsEvents = true
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_Result", Name = "A_ARG_TYPE_Result",
DataType = "int", DataType = "int",
SendsEvents = false SendsEvents = false
}); }
};
return list; return list;
} }

@ -1,13 +1,19 @@
#pragma warning disable CS1591
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {
public class ServiceActionListBuilder /// <summary>
/// Defines the <see cref="ServiceActionListBuilder" />.
/// </summary>
public static class ServiceActionListBuilder
{ {
public IEnumerable<ServiceAction> GetActions() /// <summary>
/// Returns a list of services that this instance provides.
/// </summary>
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
public static IEnumerable<ServiceAction> GetActions()
{ {
return new[] return new[]
{ {
@ -21,6 +27,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
}; };
} }
/// <summary>
/// Returns the action details for "IsValidated".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetIsValidated() private static ServiceAction GetIsValidated()
{ {
var action = new ServiceAction var action = new ServiceAction
@ -43,6 +53,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
/// <summary>
/// Returns the action details for "IsAuthorized".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetIsAuthorized() private static ServiceAction GetIsAuthorized()
{ {
var action = new ServiceAction var action = new ServiceAction
@ -65,6 +79,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
/// <summary>
/// Returns the action details for "RegisterDevice".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetRegisterDevice() private static ServiceAction GetRegisterDevice()
{ {
var action = new ServiceAction var action = new ServiceAction
@ -87,6 +105,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
/// <summary>
/// Returns the action details for "GetValidationSucceededUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetValidationSucceededUpdateID() private static ServiceAction GetGetValidationSucceededUpdateID()
{ {
var action = new ServiceAction var action = new ServiceAction
@ -103,7 +125,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
private ServiceAction GetGetAuthorizationDeniedUpdateID() /// <summary>
/// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetAuthorizationDeniedUpdateID()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@ -119,7 +145,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
private ServiceAction GetGetValidationRevokedUpdateID() /// <summary>
/// Returns the action details for "GetValidationRevokedUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetValidationRevokedUpdateID()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@ -135,7 +165,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action; return action;
} }
private ServiceAction GetGetAuthorizationGrantedUpdateID() /// <summary>
/// Returns the action details for "GetAuthorizationGrantedUpdateID".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetGetAuthorizationGrantedUpdateID()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {

@ -326,7 +326,7 @@ namespace Emby.Dlna.PlayTo
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
{ {
_logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand); _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId); var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
@ -339,7 +339,7 @@ namespace Emby.Dlna.PlayTo
var startIndex = command.StartIndex ?? 0; var startIndex = command.StartIndex ?? 0;
if (startIndex > 0) if (startIndex > 0)
{ {
items = items.Skip(startIndex).ToList(); items = items.GetRange(startIndex, items.Count - startIndex);
} }
var playlist = new List<PlaylistItem>(); var playlist = new List<PlaylistItem>();

@ -235,13 +235,13 @@ namespace Emby.Dlna.Server
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty)) .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
.Append("</serviceId>"); .Append("</serviceId>");
builder.Append("<SCPDURL>") builder.Append("<SCPDURL>")
.Append(BuildUrl(service.ScpdUrl, true)) .Append(BuildUrl(service.ScpdUrl))
.Append("</SCPDURL>"); .Append("</SCPDURL>");
builder.Append("<controlURL>") builder.Append("<controlURL>")
.Append(BuildUrl(service.ControlUrl, true)) .Append(BuildUrl(service.ControlUrl))
.Append("</controlURL>"); .Append("</controlURL>");
builder.Append("<eventSubURL>") builder.Append("<eventSubURL>")
.Append(BuildUrl(service.EventSubUrl, true)) .Append(BuildUrl(service.EventSubUrl))
.Append("</eventSubURL>"); .Append("</eventSubURL>");
builder.Append("</service>"); builder.Append("</service>");
@ -250,13 +250,7 @@ namespace Emby.Dlna.Server
builder.Append("</serviceList>"); builder.Append("</serviceList>");
} }
/// <summary> private string BuildUrl(string url)
/// Builds a valid url for inclusion in the xml.
/// </summary>
/// <param name="url">Url to include.</param>
/// <param name="absoluteUrl">Optional. When set to true, the absolute url is always used.</param>
/// <returns>The url to use for the element.</returns>
private string BuildUrl(string url, bool absoluteUrl = false)
{ {
if (string.IsNullOrEmpty(url)) if (string.IsNullOrEmpty(url))
{ {
@ -267,7 +261,7 @@ namespace Emby.Dlna.Server
url = "/dlna/" + _serverUdn + "/" + url; url = "/dlna/" + _serverUdn + "/" + url;
if (EnableAbsoluteUrls || absoluteUrl) if (EnableAbsoluteUrls)
{ {
url = _serverAddress.TrimEnd('/') + url; url = _serverAddress.TrimEnd('/') + url;
} }

@ -60,10 +60,8 @@ namespace Emby.Dlna.Service
Async = true Async = true
}; };
using (var reader = XmlReader.Create(streamReader, readerSettings)) using var reader = XmlReader.Create(streamReader, readerSettings);
{ requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
}
} }
Logger.LogDebug("Received control request {0}", requestInfo.LocalName); Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
@ -124,10 +122,8 @@ namespace Emby.Dlna.Service
{ {
if (!reader.IsEmptyElement) if (!reader.IsEmptyElement)
{ {
using (var subReader = reader.ReadSubtree()) using var subReader = reader.ReadSubtree();
{ return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
}
} }
else else
{ {
@ -150,12 +146,12 @@ namespace Emby.Dlna.Service
} }
} }
return new ControlRequestInfo(); throw new EndOfStreamException("Stream ended but no body tag found.");
} }
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader) private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
{ {
var result = new ControlRequestInfo(); string namespaceURI = null, localName = null;
await reader.MoveToContentAsync().ConfigureAwait(false); await reader.MoveToContentAsync().ConfigureAwait(false);
await reader.ReadAsync().ConfigureAwait(false); await reader.ReadAsync().ConfigureAwait(false);
@ -165,16 +161,15 @@ namespace Emby.Dlna.Service
{ {
if (reader.NodeType == XmlNodeType.Element) if (reader.NodeType == XmlNodeType.Element)
{ {
result.LocalName = reader.LocalName; localName = reader.LocalName;
result.NamespaceURI = reader.NamespaceURI; namespaceURI = reader.NamespaceURI;
if (!reader.IsEmptyElement) if (!reader.IsEmptyElement)
{ {
using (var subReader = reader.ReadSubtree()) var result = new ControlRequestInfo(localName, namespaceURI);
{ using var subReader = reader.ReadSubtree();
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false); await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
return result; return result;
}
} }
else else
{ {
@ -187,7 +182,12 @@ namespace Emby.Dlna.Service
} }
} }
return result; if (localName != null && namespaceURI != null)
{
return new ControlRequestInfo(localName, namespaceURI);
}
throw new EndOfStreamException("Stream ended but no control found.");
} }
private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers) private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
@ -234,11 +234,18 @@ namespace Emby.Dlna.Service
private class ControlRequestInfo private class ControlRequestInfo
{ {
public ControlRequestInfo(string localName, string namespaceUri)
{
LocalName = localName;
NamespaceURI = namespaceUri;
Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public string LocalName { get; set; } public string LocalName { get; set; }
public string NamespaceURI { get; set; } public string NamespaceURI { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); public Dictionary<string, string> Headers { get; }
} }
} }
} }

@ -36,7 +36,7 @@ namespace Emby.Drawing
private readonly IImageEncoder _imageEncoder; private readonly IImageEncoder _imageEncoder;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private bool _disposed = false; private bool _disposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageProcessor"/> class. /// Initializes a new instance of the <see cref="ImageProcessor"/> class.
@ -466,11 +466,11 @@ namespace Emby.Drawing
} }
/// <inheritdoc /> /// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options) public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{ {
_logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath); _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
_imageEncoder.CreateImageCollage(options); _imageEncoder.CreateImageCollage(options, libraryName);
_logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath); _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
} }

@ -38,7 +38,7 @@ namespace Emby.Drawing
} }
/// <inheritdoc /> /// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options) public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

@ -1,3 +1,4 @@
#nullable enable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -16,21 +17,11 @@ namespace Emby.Naming.AudioBook
_options = options; _options = options;
} }
public AudioBookFileInfo ParseFile(string path) public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
{ {
return Resolve(path, false); if (path.Length == 0)
}
public AudioBookFileInfo ParseDirectory(string path)
{
return Resolve(path, true);
}
public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
{
if (string.IsNullOrEmpty(path))
{ {
throw new ArgumentNullException(nameof(path)); throw new ArgumentException("String can't be empty.", nameof(path));
} }
// TODO // TODO

@ -15,6 +15,11 @@ namespace Emby.Naming.Video
public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes) public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
{ {
CleanDateTimeResult result = new CleanDateTimeResult(name); CleanDateTimeResult result = new CleanDateTimeResult(name);
if (string.IsNullOrEmpty(name))
{
return result;
}
var len = cleanDateTimeRegexes.Count; var len = cleanDateTimeRegexes.Count;
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
{ {

@ -209,7 +209,10 @@ namespace Emby.Notifications
_libraryUpdateTimer = null; _libraryUpdateTimer = null;
} }
items = items.Take(10).ToList(); if (items.Count > 10)
{
items = items.GetRange(0, 10);
}
foreach (var item in items) foreach (var item in items)
{ {

@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.AppBase
} }
/// <inheritdoc /> /// <inheritdoc />
public string VirtualDataPath { get; } = "%AppDataPath%"; public string VirtualDataPath => "%AppDataPath%";
/// <summary> /// <summary>
/// Gets the image cache path. /// Gets the image cache path.

@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Dto; using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO; using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Library;
@ -127,7 +125,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder; private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private IHttpClientFactory _httpClientFactory; private IHttpClientFactory _httpClientFactory;
private IWebSocketManager _webSocketManager;
private string[] _urlPrefixes; private string[] _urlPrefixes;
@ -258,8 +255,8 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection) IServiceCollection serviceCollection)
{ {
_xmlSerializer = new MyXmlSerializer(); _xmlSerializer = new MyXmlSerializer();
_jsonSerializer = new JsonSerializer(); _jsonSerializer = new JsonSerializer();
ServiceCollection = serviceCollection; ServiceCollection = serviceCollection;
_networkManager = networkManager; _networkManager = networkManager;
@ -339,7 +336,7 @@ namespace Emby.Server.Implementations
/// Gets the email address for use within a comment section of a user agent field. /// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service. /// Presently used to provide contact information to MusicBrainz service.
/// </summary> /// </summary>
public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org"; public string ApplicationUserAgentAddress => "team@jellyfin.org";
/// <summary> /// <summary>
/// Gets the current application name. /// Gets the current application name.
@ -403,7 +400,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Resolves this instance. /// Resolves this instance.
/// </summary> /// </summary>
/// <typeparam name="T">The type</typeparam> /// <typeparam name="T">The type.</typeparam>
/// <returns>``0.</returns> /// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>(); public T Resolve<T>() => ServiceProvider.GetService<T>();
@ -665,7 +662,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>(); _mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>(); _sessionManager = Resolve<ISessionManager>();
_httpClientFactory = Resolve<IHttpClientFactory>(); _httpClientFactory = Resolve<IHttpClientFactory>();
_webSocketManager = Resolve<IWebSocketManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@ -786,7 +782,6 @@ namespace Emby.Server.Implementations
.ToArray(); .ToArray();
_urlPrefixes = GetUrlPrefixes().ToArray(); _urlPrefixes = GetUrlPrefixes().ToArray();
_webSocketManager.Init(GetExports<IWebSocketListener>());
Resolve<ILibraryManager>().AddParts( Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(), GetExports<IResolverIgnoreRule>(),
@ -819,38 +814,6 @@ namespace Emby.Server.Implementations
{ {
try try
{ {
if (plugin is IPluginAssembly assemblyPlugin)
{
var assembly = plugin.GetType().Assembly;
var assemblyName = assembly.GetName();
var assemblyFilePath = assembly.Location;
var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
try
{
var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
if (idAttributes.Length > 0)
{
var attribute = (GuidAttribute)idAttributes[0];
var assemblyId = new Guid(attribute.Value);
assemblyPlugin.SetId(assemblyId);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName);
}
}
if (plugin is IHasPluginConfiguration hasPluginConfiguration)
{
hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
}
plugin.RegisterServices(ServiceCollection); plugin.RegisterServices(ServiceCollection);
} }
catch (Exception ex) catch (Exception ex)
@ -1026,80 +989,54 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal(); protected abstract void RestartInternal();
/// <summary> /// <inheritdoc/>
/// Comparison function used in <see cref="GetPlugins" />. public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
/// </summary>
/// <param name="a">Item to compare.</param>
/// <param name="b">Item to compare with.</param>
/// <returns>Boolean result of the operation.</returns>
private static int VersionCompare(
(Version PluginVersion, string Name, string Path) a,
(Version PluginVersion, string Name, string Path) b)
{
int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
if (compare == 0)
{
return a.PluginVersion.CompareTo(b.PluginVersion);
}
return compare;
}
/// <summary>
/// Returns a list of plugins to install.
/// </summary>
/// <param name="path">Path to check.</param>
/// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
/// <returns>Enumerable list of dlls to load.</returns>
private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
{ {
var dllList = new List<string>(); var minimumVersion = new Version(0, 0, 0, 1);
var versions = new List<(Version PluginVersion, string Name, string Path)>(); var versions = new List<LocalPlugin>();
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly); var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
string metafile;
foreach (var dir in directories) foreach (var dir in directories)
{ {
try try
{ {
metafile = Path.Combine(dir, "meta.json"); var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile)) if (File.Exists(metafile))
{ {
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile); var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi)) if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{ {
targetAbi = new Version(0, 0, 0, 1); targetAbi = minimumVersion;
} }
if (!Version.TryParse(manifest.Version, out var version)) if (!Version.TryParse(manifest.Version, out var version))
{ {
version = new Version(0, 0, 0, 1); version = minimumVersion;
} }
if (ApplicationVersion >= targetAbi) if (ApplicationVersion >= targetAbi)
{ {
// Only load Plugins if the plugin is built for this version or below. // Only load Plugins if the plugin is built for this version or below.
versions.Add((version, manifest.Name, dir)); versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
} }
} }
else else
{ {
// No metafile, so lets see if the folder is versioned. // No metafile, so lets see if the folder is versioned.
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1]; metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_'); int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver)) if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
{ {
// Versioned folder. // Versioned folder.
versions.Add((ver, metafile, dir)); versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
} }
else else
{ {
// Un-versioned folder - Add it under the path name and version 0.0.0.1. // Un-versioned folder - Add it under the path name and version 0.0.0.1.
versions.Add((new Version(0, 0, 0, 1), metafile, dir)); versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
} }
} }
} }
catch catch
@ -1109,14 +1046,14 @@ namespace Emby.Server.Implementations
} }
string lastName = string.Empty; string lastName = string.Empty;
versions.Sort(VersionCompare); versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list. // Traverse backwards through the list.
// The first item will be the latest version. // The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--) for (int x = versions.Count - 1; x >= 0; x--)
{ {
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
{ {
dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories)); versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
lastName = versions[x].Name; lastName = versions[x].Name;
continue; continue;
} }
@ -1124,6 +1061,7 @@ namespace Emby.Server.Implementations
if (!string.IsNullOrEmpty(lastName) && cleanup) if (!string.IsNullOrEmpty(lastName) && cleanup)
{ {
// Attempt a cleanup of old folders. // Attempt a cleanup of old folders.
versions.RemoveAt(x);
try try
{ {
Logger.LogDebug("Deleting {Path}", versions[x].Path); Logger.LogDebug("Deleting {Path}", versions[x].Path);
@ -1136,7 +1074,7 @@ namespace Emby.Server.Implementations
} }
} }
return dllList; return versions;
} }
/// <summary> /// <summary>
@ -1147,21 +1085,24 @@ namespace Emby.Server.Implementations
{ {
if (Directory.Exists(ApplicationPaths.PluginsPath)) if (Directory.Exists(ApplicationPaths.PluginsPath))
{ {
foreach (var file in GetPlugins(ApplicationPaths.PluginsPath)) foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
{ {
Assembly plugAss; foreach (var file in plugin.DllFiles)
try
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{ {
Logger.LogError(ex, "Failed to load assembly {Path}", file); Assembly plugAss;
continue; try
} {
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file); Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss; yield return plugAss;
}
} }
} }

@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
var all = channels; var all = channels;
var totalCount = all.Count; var totalCount = all.Count;
if (query.StartIndex.HasValue) if (query.StartIndex.HasValue || query.Limit.HasValue)
{ {
all = all.Skip(query.StartIndex.Value).ToList(); int startIndex = query.StartIndex ?? 0;
int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
all = all.GetRange(startIndex, count);
} }
if (query.Limit.HasValue)
{
all = all.Take(query.Limit.Value).ToList();
}
var returnItems = all.ToArray();
if (query.RefreshLatestChannelItems) if (query.RefreshLatestChannelItems)
{ {
foreach (var item in returnItems) foreach (var item in all)
{ {
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult(); RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
} }
@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
return new QueryResult<Channel> return new QueryResult<Channel>
{ {
Items = returnItems, Items = all,
TotalRecordCount = totalCount TotalRecordCount = totalCount
}; };
} }
@ -543,7 +538,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemIds( return _libraryManager.GetItemIds(
new InternalItemsQuery new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Channel).Name }, IncludeItemTypes = new[] { nameof(Channel) },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
} }

@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Channels
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Channel).Name }, IncludeItemTypes = new[] { nameof(Channel) },
ExcludeItemIds = installedChannelIds.ToArray() ExcludeItemIds = installedChannelIds.ToArray()
}); });

@ -157,7 +157,8 @@ namespace Emby.Server.Implementations.Data
protected bool TableExists(ManagedConnection connection, string name) protected bool TableExists(ManagedConnection connection, string name)
{ {
return connection.RunInTransaction(db => return connection.RunInTransaction(
db =>
{ {
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master")) using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
{ {

@ -219,7 +219,8 @@ namespace Emby.Server.Implementations.Data
{ {
connection.RunQueries(queries); connection.RunQueries(queries);
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
var existingColumnNames = GetColumnNames(db, "AncestorIds"); var existingColumnNames = GetColumnNames(db, "AncestorIds");
AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
@ -495,7 +496,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{ {
@ -546,7 +548,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
SaveItemsInTranscation(db, tuples); SaveItemsInTranscation(db, tuples);
}, TransactionMode); }, TransactionMode);
@ -2032,7 +2035,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
// First delete chapters // First delete chapters
db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
@ -2399,11 +2403,11 @@ namespace Emby.Server.Implementations.Data
if (string.IsNullOrEmpty(item.OfficialRating)) if (string.IsNullOrEmpty(item.OfficialRating))
{ {
builder.Append("((OfficialRating is null) * 10)"); builder.Append("(OfficialRating is null * 10)");
} }
else else
{ {
builder.Append("((OfficialRating=@ItemOfficialRating) * 10)"); builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
} }
if (item.ProductionYear.HasValue) if (item.ProductionYear.HasValue)
@ -2412,8 +2416,26 @@ namespace Emby.Server.Implementations.Data
builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )"); builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
} }
//// genres, tags // genres, tags, studios, person, year?
builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)"); builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
if (item is MusicArtist)
{
// Match albums where the artist is AlbumArtist against other albums.
// It is assumed that similar albums => similar artists.
builder.Append(
@"+ (WITH artistValues AS (
SELECT DISTINCT albumValues.CleanValue
FROM ItemValues albumValues
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
), similarArtist AS (
SELECT albumValues.ItemId
FROM ItemValues albumValues
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
}
builder.Append(") as SimilarityScore"); builder.Append(") as SimilarityScore");
@ -2921,7 +2943,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<BaseItem>(); var result = new QueryResult<BaseItem>();
using (var connection = GetConnection(true)) using (var connection = GetConnection(true))
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
var statements = PrepareAll(db, statementTexts); var statements = PrepareAll(db, statementTexts);
@ -3324,7 +3347,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<Guid>(); var result = new QueryResult<Guid>();
using (var connection = GetConnection(true)) using (var connection = GetConnection(true))
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
var statements = PrepareAll(db, statementTexts); var statements = PrepareAll(db, statementTexts);
@ -3908,7 +3932,7 @@ namespace Emby.Server.Implementations.Data
if (query.IsPlayed.HasValue) if (query.IsPlayed.HasValue)
{ {
// We should probably figure this out for all folders, but for right now, this is the only place where we need it // We should probably figure this out for all folders, but for right now, this is the only place where we need it
if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase)) if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
{ {
if (query.IsPlayed.Value) if (query.IsPlayed.Value)
{ {
@ -4749,29 +4773,29 @@ namespace Emby.Server.Implementations.Data
{ {
var list = new List<string>(); var list = new List<string>();
if (IsTypeInQuery(typeof(Person).Name, query)) if (IsTypeInQuery(nameof(Person), query))
{ {
list.Add(typeof(Person).Name); list.Add(nameof(Person));
} }
if (IsTypeInQuery(typeof(Genre).Name, query)) if (IsTypeInQuery(nameof(Genre), query))
{ {
list.Add(typeof(Genre).Name); list.Add(nameof(Genre));
} }
if (IsTypeInQuery(typeof(MusicGenre).Name, query)) if (IsTypeInQuery(nameof(MusicGenre), query))
{ {
list.Add(typeof(MusicGenre).Name); list.Add(nameof(MusicGenre));
} }
if (IsTypeInQuery(typeof(MusicArtist).Name, query)) if (IsTypeInQuery(nameof(MusicArtist), query))
{ {
list.Add(typeof(MusicArtist).Name); list.Add(nameof(MusicArtist));
} }
if (IsTypeInQuery(typeof(Studio).Name, query)) if (IsTypeInQuery(nameof(Studio), query))
{ {
list.Add(typeof(Studio).Name); list.Add(nameof(Studio));
} }
return list; return list;
@ -4826,12 +4850,12 @@ namespace Emby.Server.Implementations.Data
var types = new[] var types = new[]
{ {
typeof(Episode).Name, nameof(Episode),
typeof(Video).Name, nameof(Video),
typeof(Movie).Name, nameof(Movie),
typeof(MusicVideo).Name, nameof(MusicVideo),
typeof(Series).Name, nameof(Series),
typeof(Season).Name nameof(Season)
}; };
if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase))) if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
@ -4899,7 +4923,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
connection.ExecuteAll(sql); connection.ExecuteAll(sql);
}, TransactionMode); }, TransactionMode);
@ -4950,7 +4975,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
var idBlob = id.ToByteArray(); var idBlob = id.ToByteArray();
@ -4994,26 +5020,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed(); CheckDisposed();
var commandText = "select Distinct Name from People"; var commandText = new StringBuilder("select Distinct p.Name from People p");
if (query.User != null && query.IsFavorite.HasValue)
{
commandText.Append(" LEFT JOIN TypedBaseItems tbi ON tbi.Name=p.Name AND tbi.Type='");
commandText.Append(typeof(Person).FullName);
commandText.Append("' LEFT JOIN UserDatas ON tbi.UserDataKey=key AND userId=@UserId");
}
var whereClauses = GetPeopleWhereClauses(query, null); var whereClauses = GetPeopleWhereClauses(query, null);
if (whereClauses.Count != 0) if (whereClauses.Count != 0)
{ {
commandText += " where " + string.Join(" AND ", whereClauses); commandText.Append(" where ").Append(string.Join(" AND ", whereClauses));
} }
commandText += " order by ListOrder"; commandText.Append(" order by ListOrder");
if (query.Limit > 0) if (query.Limit > 0)
{ {
commandText += " LIMIT " + query.Limit; commandText.Append(" LIMIT ").Append(query.Limit);
} }
using (var connection = GetConnection(true)) using (var connection = GetConnection(true))
{ {
var list = new List<string>(); var list = new List<string>();
using (var statement = PrepareStatement(connection, commandText)) using (var statement = PrepareStatement(connection, commandText.ToString()))
{ {
// Run this again to bind the params // Run this again to bind the params
GetPeopleWhereClauses(query, statement); GetPeopleWhereClauses(query, statement);
@ -5037,7 +5070,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed(); CheckDisposed();
var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People"; var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p";
var whereClauses = GetPeopleWhereClauses(query, null); var whereClauses = GetPeopleWhereClauses(query, null);
@ -5079,19 +5112,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (!query.ItemId.Equals(Guid.Empty)) if (!query.ItemId.Equals(Guid.Empty))
{ {
whereClauses.Add("ItemId=@ItemId"); whereClauses.Add("ItemId=@ItemId");
if (statement != null) statement?.TryBind("@ItemId", query.ItemId.ToByteArray());
{
statement.TryBind("@ItemId", query.ItemId.ToByteArray());
}
} }
if (!query.AppearsInItemId.Equals(Guid.Empty)) if (!query.AppearsInItemId.Equals(Guid.Empty))
{ {
whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)"); whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
if (statement != null) statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
{
statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
}
} }
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList(); var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
@ -5099,10 +5126,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryPersonTypes.Count == 1) if (queryPersonTypes.Count == 1)
{ {
whereClauses.Add("PersonType=@PersonType"); whereClauses.Add("PersonType=@PersonType");
if (statement != null) statement?.TryBind("@PersonType", queryPersonTypes[0]);
{
statement.TryBind("@PersonType", queryPersonTypes[0]);
}
} }
else if (queryPersonTypes.Count > 1) else if (queryPersonTypes.Count > 1)
{ {
@ -5116,10 +5140,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryExcludePersonTypes.Count == 1) if (queryExcludePersonTypes.Count == 1)
{ {
whereClauses.Add("PersonType<>@PersonType"); whereClauses.Add("PersonType<>@PersonType");
if (statement != null) statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
{
statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
}
} }
else if (queryExcludePersonTypes.Count > 1) else if (queryExcludePersonTypes.Count > 1)
{ {
@ -5131,19 +5152,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (query.MaxListOrder.HasValue) if (query.MaxListOrder.HasValue)
{ {
whereClauses.Add("ListOrder<=@MaxListOrder"); whereClauses.Add("ListOrder<=@MaxListOrder");
if (statement != null) statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
{
statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
}
} }
if (!string.IsNullOrWhiteSpace(query.NameContains)) if (!string.IsNullOrWhiteSpace(query.NameContains))
{ {
whereClauses.Add("Name like @NameContains"); whereClauses.Add("p.Name like @NameContains");
if (statement != null) statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
{ }
statement.TryBind("@NameContains", "%" + query.NameContains + "%");
} if (query.IsFavorite.HasValue)
{
whereClauses.Add("isFavorite=@IsFavorite");
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
}
if (query.User != null)
{
statement?.TryBind("@UserId", query.User.InternalId);
} }
return whereClauses; return whereClauses;
@ -5357,7 +5383,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
itemCountColumns = new Dictionary<string, string>() itemCountColumns = new Dictionary<string, string>()
{ {
{ "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes"} { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
}; };
} }
@ -5412,6 +5438,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
NameStartsWithOrGreater = query.NameStartsWithOrGreater, NameStartsWithOrGreater = query.NameStartsWithOrGreater,
Tags = query.Tags, Tags = query.Tags,
OfficialRatings = query.OfficialRatings, OfficialRatings = query.OfficialRatings,
StudioIds = query.StudioIds,
GenreIds = query.GenreIds, GenreIds = query.GenreIds,
Genres = query.Genres, Genres = query.Genres,
Years = query.Years, Years = query.Years,
@ -5744,7 +5771,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
var itemIdBlob = itemId.ToByteArray(); var itemIdBlob = itemId.ToByteArray();
@ -5898,7 +5926,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
var itemIdBlob = id.ToByteArray(); var itemIdBlob = id.ToByteArray();
@ -6232,7 +6261,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
var itemIdBlob = id.ToByteArray(); var itemIdBlob = id.ToByteArray();

@ -44,7 +44,8 @@ namespace Emby.Server.Implementations.Data
var users = userDatasTableExists ? null : userManager.Users; var users = userDatasTableExists ? null : userManager.Users;
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
db.ExecuteAll(string.Join(";", new[] { db.ExecuteAll(string.Join(";", new[] {
@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
SaveUserData(db, internalUserId, key, userData); SaveUserData(db, internalUserId, key, userData);
}, TransactionMode); }, TransactionMode);
@ -246,7 +248,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(
db =>
{ {
foreach (var userItemData in userDataList) foreach (var userItemData in userDataList)
{ {

@ -465,7 +465,7 @@ namespace Emby.Server.Implementations.Dto
{ {
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, IncludeItemTypes = new[] { nameof(MusicAlbum) },
Name = item.Album, Name = item.Album,
Limit = 1 Limit = 1
}); });

@ -32,10 +32,10 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.8" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.8" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.8" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
<PackageReference Include="Mono.Nat" Version="3.0.0" /> <PackageReference Include="Mono.Nat" Version="3.0.0" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />

@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo Authenticate(HttpRequest request) public AuthorizationInfo Authenticate(HttpRequest request)
{ {
var auth = _authorizationContext.GetAuthorizationInfo(request); var auth = _authorizationContext.GetAuthorizationInfo(request);
if (auth?.User == null) if (!auth.IsAuthenticated)
{ {
return null; throw new AuthenticationException("Invalid token.");
} }
if (auth.User.HasPermission(PermissionKind.IsDisabled)) if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
{ {
throw new SecurityException("User account has been disabled."); throw new SecurityException("User account has been disabled.");
} }

@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext) public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{ {
var auth = GetAuthorizationDictionary(requestContext); var auth = GetAuthorizationDictionary(requestContext);
var (authInfo, _) = var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo; return authInfo;
} }
@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(HttpContext httpReq) private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{ {
var auth = GetAuthorizationDictionary(httpReq); var auth = GetAuthorizationDictionary(httpReq);
var (authInfo, originalAuthInfo) = var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
if (originalAuthInfo != null)
{
httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
}
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo; return authInfo;
} }
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary( private AuthorizationInfo GetAuthorizationInfoFromDictionary(
in Dictionary<string, string> auth, in Dictionary<string, string> auth,
in IHeaderDictionary headers, in IHeaderDictionary headers,
in IQueryCollection queryString) in IQueryCollection queryString)
@ -108,88 +101,102 @@ namespace Emby.Server.Implementations.HttpServer.Security
Device = device, Device = device,
DeviceId = deviceId, DeviceId = deviceId,
Version = version, Version = version,
Token = token Token = token,
IsAuthenticated = false
}; };
AuthenticationInfo originalAuthenticationInfo = null; if (string.IsNullOrWhiteSpace(token))
if (!string.IsNullOrWhiteSpace(token))
{ {
var result = _authRepo.Get(new AuthenticationInfoQuery // Request doesn't contain a token.
{ return authInfo;
AccessToken = token }
});
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null; var result = _authRepo.Get(new AuthenticationInfoQuery
{
AccessToken = token
});
if (originalAuthenticationInfo != null) if (result.Items.Count > 0)
{ {
var updateToken = false; authInfo.IsAuthenticated = true;
}
// TODO: Remove these checks for IsNullOrWhiteSpace var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
if (string.IsNullOrWhiteSpace(authInfo.Client))
{
authInfo.Client = originalAuthenticationInfo.AppName;
}
if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) if (originalAuthenticationInfo != null)
{ {
authInfo.DeviceId = originalAuthenticationInfo.DeviceId; var updateToken = false;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device // TODO: Remove these checks for IsNullOrWhiteSpace
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1; if (string.IsNullOrWhiteSpace(authInfo.Client))
{
authInfo.Client = originalAuthenticationInfo.AppName;
}
if (string.IsNullOrWhiteSpace(authInfo.Device)) if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{ {
authInfo.Device = originalAuthenticationInfo.DeviceName; authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
} }
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(authInfo.Version)) // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
{ var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
authInfo.Version = originalAuthenticationInfo.AppVersion;
} if (string.IsNullOrWhiteSpace(authInfo.Device))
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) {
authInfo.Device = originalAuthenticationInfo.DeviceName;
}
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{ {
if (allowTokenInfoUpdate) updateToken = true;
{ originalAuthenticationInfo.DeviceName = authInfo.Device;
updateToken = true;
originalAuthenticationInfo.AppVersion = authInfo.Version;
}
} }
}
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3) if (string.IsNullOrWhiteSpace(authInfo.Version))
{
authInfo.Version = originalAuthenticationInfo.AppVersion;
}
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{ {
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true; updateToken = true;
originalAuthenticationInfo.AppVersion = authInfo.Version;
} }
}
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty)) if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{ {
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId); originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase)) if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{ {
originalAuthenticationInfo.UserName = authInfo.User.Username; authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
updateToken = true;
}
}
if (updateToken) if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{ {
_authRepo.Update(originalAuthenticationInfo); originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
} }
authInfo.IsApiKey = true;
}
else
{
authInfo.IsApiKey = false;
}
if (updateToken)
{
_authRepo.Update(originalAuthenticationInfo);
} }
} }
return (authInfo, originalAuthenticationInfo); return authInfo;
} }
/// <summary> /// <summary>
@ -267,7 +274,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (param.Length == 2) if (param.Length == 2)
{ {
var value = NormalizeValue(param[1].Trim(new[] { '"' })); var value = NormalizeValue(param[1].Trim(new[] { '"' }));
result.Add(param[0], value); result[param[0]] = value;
} }
} }

@ -2,7 +2,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
@ -14,16 +13,18 @@ namespace Emby.Server.Implementations.HttpServer
{ {
public class WebSocketManager : IWebSocketManager public class WebSocketManager : IWebSocketManager
{ {
private readonly Lazy<IEnumerable<IWebSocketListener>> _webSocketListeners;
private readonly ILogger<WebSocketManager> _logger; private readonly ILogger<WebSocketManager> _logger;
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
private bool _disposed = false; private bool _disposed = false;
public WebSocketManager( public WebSocketManager(
Lazy<IEnumerable<IWebSocketListener>> webSocketListeners,
ILogger<WebSocketManager> logger, ILogger<WebSocketManager> logger,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
{ {
_webSocketListeners = webSocketListeners;
_logger = logger; _logger = logger;
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
} }
@ -68,15 +69,6 @@ namespace Emby.Server.Implementations.HttpServer
} }
} }
/// <summary>
/// Adds the rest handlers.
/// </summary>
/// <param name="listeners">The web socket listeners.</param>
public void Init(IEnumerable<IWebSocketListener> listeners)
{
_webSocketListeners = listeners.ToArray();
}
/// <summary> /// <summary>
/// Processes the web socket message received. /// Processes the web socket message received.
/// </summary> /// </summary>
@ -90,7 +82,8 @@ namespace Emby.Server.Implementations.HttpServer
IEnumerable<Task> GetTasks() IEnumerable<Task> GetTasks()
{ {
foreach (var x in _webSocketListeners) var listeners = _webSocketListeners.Value;
foreach (var x in listeners)
{ {
yield return x.ProcessMessageAsync(result); yield return x.ProcessMessageAsync(result);
} }

@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Images
// return _libraryManager.GetItemList(new InternalItemsQuery // return _libraryManager.GetItemList(new InternalItemsQuery
// { // {
// ArtistIds = new[] { item.Id }, // ArtistIds = new[] { item.Id },
// IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, // IncludeItemTypes = new[] { nameof(MusicAlbum) },
// OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, // OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
// Limit = 4, // Limit = 4,
// Recursive = true, // Recursive = true,

@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images
protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items) protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items)
{ {
var useBackdrop = primaryItem is CollectionFolder;
return items return items
.Select(i => .Select(i =>
{ {
// Use Backdrop instead of Primary image for Library images.
if (useBackdrop)
{
var backdrop = i.GetImageInfo(ImageType.Backdrop, 0);
if (backdrop != null && backdrop.IsLocalFile)
{
return backdrop.Path;
}
}
var image = i.GetImageInfo(ImageType.Primary, 0); var image = i.GetImageInfo(ImageType.Primary, 0);
if (image != null && image.IsLocalFile) if (image != null && image.IsLocalFile)
{ {
@ -190,7 +201,7 @@ namespace Emby.Server.Implementations.Images
return null; return null;
} }
ImageProcessor.CreateImageCollage(options); ImageProcessor.CreateImageCollage(options, primaryItem.Name);
return outputPath; return outputPath;
} }

@ -42,7 +42,12 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery return _libraryManager.GetItemList(new InternalItemsQuery
{ {
Genres = new[] { item.Name }, Genres = new[] { item.Name },
IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name }, IncludeItemTypes = new[]
{
nameof(MusicAlbum),
nameof(MusicVideo),
nameof(Audio)
},
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4, Limit = 4,
Recursive = true, Recursive = true,
@ -77,7 +82,7 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery return _libraryManager.GetItemList(new InternalItemsQuery
{ {
Genres = new[] { item.Name }, Genres = new[] { item.Name },
IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name }, IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4, Limit = 4,
Recursive = true, Recursive = true,

@ -2440,6 +2440,21 @@ namespace Emby.Server.Implementations.Library
new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
} }
public BaseItem GetParentItem(string parentId, Guid? userId)
{
if (!string.IsNullOrEmpty(parentId))
{
return GetItemById(new Guid(parentId));
}
if (userId.HasValue && userId != Guid.Empty)
{
return GetUserRootFolder();
}
return RootFolder;
}
/// <inheritdoc /> /// <inheritdoc />
public bool IsVideoFile(string path) public bool IsVideoFile(string path)
{ {

@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager; private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private IMediaSourceProvider[] _providers; private IMediaSourceProvider[] _providers;
@ -582,29 +583,20 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate(); mediaSource.InferTotalBitrate();
} }
public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{ {
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); var info = _openStreams.Values.FirstOrDefault(i =>
try
{ {
var info = _openStreams.Values.FirstOrDefault(i => var liveStream = i as ILiveStream;
if (liveStream != null)
{ {
var liveStream = i as ILiveStream; return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
if (liveStream != null) }
{
return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
}
return false; return false;
}); });
return info as IDirectStreamProvider; return Task.FromResult(info as IDirectStreamProvider);
}
finally
{
_liveStreamSemaphore.Release();
}
} }
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
@ -793,29 +785,20 @@ namespace Emby.Server.Implementations.Library
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider); return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
} }
private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken) private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(id)) if (string.IsNullOrEmpty(id))
{ {
throw new ArgumentNullException(nameof(id)); throw new ArgumentNullException(nameof(id));
} }
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); if (_openStreams.TryGetValue(id, out ILiveStream info))
try
{ {
if (_openStreams.TryGetValue(id, out ILiveStream info)) return Task.FromResult(info);
{
return info;
}
else
{
throw new ResourceNotFoundException();
}
} }
finally else
{ {
_liveStreamSemaphore.Release(); return Task.FromException<ILiveStream>(new ResourceNotFoundException());
} }
} }
@ -844,7 +827,7 @@ namespace Emby.Server.Implementations.Library
if (liveStream.ConsumerCount <= 0) if (liveStream.ConsumerCount <= 0)
{ {
_openStreams.Remove(id); _openStreams.TryRemove(id, out _);
_logger.LogInformation("Closing live stream {0}", id); _logger.LogInformation("Closing live stream {0}", id);

@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Library
var genres = item var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user) .GetRecursiveChildren(user, new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(Audio).Name }, IncludeItemTypes = new[] { nameof(Audio) },
DtoOptions = dtoOptions DtoOptions = dtoOptions
}) })
.Cast<Audio>() .Cast<Audio>()
@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Library
{ {
return _libraryManager.GetItemList(new InternalItemsQuery(user) return _libraryManager.GetItemList(new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(Audio).Name }, IncludeItemTypes = new[] { nameof(Audio) },
GenreIds = genreIds.ToArray(), GenreIds = genreIds.ToArray(),

@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <value>The priority.</value> /// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Fourth; public override ResolverPriority Priority => ResolverPriority.Fourth;
public MultiItemResolverResult ResolveMultiple(Folder parent, public MultiItemResolverResult ResolveMultiple(
Folder parent,
List<FileSystemMetadata> files, List<FileSystemMetadata> files,
string collectionType, string collectionType,
IDirectoryService directoryService) IDirectoryService directoryService)
@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return result; return result;
} }
private MultiItemResolverResult ResolveMultipleInternal(Folder parent, private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files, List<FileSystemMetadata> files,
string collectionType, string collectionType,
IDirectoryService directoryService) IDirectoryService directoryService)

@ -1,5 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Audio; using Emby.Naming.Audio;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -113,52 +116,48 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
IFileSystem fileSystem, IFileSystem fileSystem,
ILibraryManager libraryManager) ILibraryManager libraryManager)
{ {
// check for audio files before digging down into directories
var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
if (foundAudioFile)
{
// at least one audio file exists
return true;
}
if (!allowSubfolders)
{
// not music since no audio file exists and we're not looking into subfolders
return false;
}
var discSubfolderCount = 0; var discSubfolderCount = 0;
var notMultiDisc = false;
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
var parser = new AlbumParser(namingOptions); var parser = new AlbumParser(namingOptions);
foreach (var fileSystemInfo in list)
var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
{ {
if (fileSystemInfo.IsDirectory) var path = fileSystemInfo.FullName;
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
if (hasMusic)
{ {
if (allowSubfolders) if (parser.IsMultiPart(path))
{ {
if (notMultiDisc) logger.LogDebug("Found multi-disc folder: " + path);
{ Interlocked.Increment(ref discSubfolderCount);
continue;
}
var path = fileSystemInfo.FullName;
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
if (hasMusic)
{
if (parser.IsMultiPart(path))
{
logger.LogDebug("Found multi-disc folder: " + path);
discSubfolderCount++;
}
else
{
// If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
notMultiDisc = true;
}
}
} }
} else
else
{
var fullName = fileSystemInfo.FullName;
if (libraryManager.IsAudioFile(fullName))
{ {
return true; // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
state.Stop();
} }
} }
} });
if (notMultiDisc) if (!result.IsCompleted)
{ {
return false; return false;
} }

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager); var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
// If we contain an album assume we are an artist folder // If we contain an album assume we are an artist folder
return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null; var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
{
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
{
// stop once we see a music album
state.Stop();
}
});
return !result.IsCompleted ? new MusicArtist() : null;
} }
} }
} }

@ -50,7 +50,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var fileExtension = Path.GetExtension(f.FullName) ?? var fileExtension = Path.GetExtension(f.FullName) ??
string.Empty; string.Empty;
return _validExtensions.Contains(fileExtension, return _validExtensions.Contains(
fileExtension,
StringComparer StringComparer
.OrdinalIgnoreCase); .OrdinalIgnoreCase);
}).ToList(); }).ToList();

@ -87,61 +87,61 @@ namespace Emby.Server.Implementations.Library
var excludeItemTypes = query.ExcludeItemTypes.ToList(); var excludeItemTypes = query.ExcludeItemTypes.ToList();
var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList(); var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
excludeItemTypes.Add(typeof(Year).Name); excludeItemTypes.Add(nameof(Year));
excludeItemTypes.Add(typeof(Folder).Name); excludeItemTypes.Add(nameof(Folder));
if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase))) if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
{ {
if (!query.IncludeMedia) if (!query.IncludeMedia)
{ {
AddIfMissing(includeItemTypes, typeof(Genre).Name); AddIfMissing(includeItemTypes, nameof(Genre));
AddIfMissing(includeItemTypes, typeof(MusicGenre).Name); AddIfMissing(includeItemTypes, nameof(MusicGenre));
} }
} }
else else
{ {
AddIfMissing(excludeItemTypes, typeof(Genre).Name); AddIfMissing(excludeItemTypes, nameof(Genre));
AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name); AddIfMissing(excludeItemTypes, nameof(MusicGenre));
} }
if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase))) if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
{ {
if (!query.IncludeMedia) if (!query.IncludeMedia)
{ {
AddIfMissing(includeItemTypes, typeof(Person).Name); AddIfMissing(includeItemTypes, nameof(Person));
} }
} }
else else
{ {
AddIfMissing(excludeItemTypes, typeof(Person).Name); AddIfMissing(excludeItemTypes, nameof(Person));
} }
if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase))) if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
{ {
if (!query.IncludeMedia) if (!query.IncludeMedia)
{ {
AddIfMissing(includeItemTypes, typeof(Studio).Name); AddIfMissing(includeItemTypes, nameof(Studio));
} }
} }
else else
{ {
AddIfMissing(excludeItemTypes, typeof(Studio).Name); AddIfMissing(excludeItemTypes, nameof(Studio));
} }
if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase))) if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
{ {
if (!query.IncludeMedia) if (!query.IncludeMedia)
{ {
AddIfMissing(includeItemTypes, typeof(MusicArtist).Name); AddIfMissing(includeItemTypes, nameof(MusicArtist));
} }
} }
else else
{ {
AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name); AddIfMissing(excludeItemTypes, nameof(MusicArtist));
} }
AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name); AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
AddIfMissing(excludeItemTypes, typeof(Folder).Name); AddIfMissing(excludeItemTypes, nameof(Folder));
var mediaTypes = query.MediaTypes.ToList(); var mediaTypes = query.MediaTypes.ToList();
if (includeItemTypes.Count > 0) if (includeItemTypes.Count > 0)

@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(MusicArtist).Name }, IncludeItemTypes = new[] { nameof(MusicArtist) },
IsDeadArtist = true, IsDeadArtist = true,
IsLocked = false IsLocked = false
}).Cast<MusicArtist>().ToList(); }).Cast<MusicArtist>().ToList();

@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Person).Name }, IncludeItemTypes = new[] { nameof(Person) },
IsDeadPerson = true, IsDeadPerson = true,
IsLocked = false IsLocked = false
}); });

@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Studio).Name }, IncludeItemTypes = new[] { nameof(Studio) },
IsDeadStudio = true, IsDeadStudio = true,
IsLocked = false IsLocked = false
}); });

@ -1790,7 +1790,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new[] { nameof(LiveTvProgram) },
Limit = 1, Limit = 1,
ExternalId = timer.ProgramId, ExternalId = timer.ProgramId,
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
@ -2151,7 +2151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
Limit = 1, Limit = 1,
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
{ {
@ -2370,7 +2370,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = seriesTimer.SeriesId, ExternalSeriesId = seriesTimer.SeriesId,
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
{ {
@ -2405,7 +2405,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList( channel = _libraryManager.GetItemList(
new InternalItemsQuery new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { parent.ChannelId }, ItemIds = new[] { parent.ChannelId },
DtoOptions = new DtoOptions() DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel; }).FirstOrDefault() as LiveTvChannel;
@ -2464,7 +2464,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList( channel = _libraryManager.GetItemList(
new InternalItemsQuery new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { programInfo.ChannelId }, ItemIds = new[] { programInfo.ChannelId },
DtoOptions = new DtoOptions() DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel; }).FirstOrDefault() as LiveTvChannel;
@ -2529,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var seriesIds = _libraryManager.GetItemIds( var seriesIds = _libraryManager.GetItemIds(
new InternalItemsQuery new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Series).Name }, IncludeItemTypes = new[] { nameof(Series) },
Name = program.Name Name = program.Name
}).ToArray(); }).ToArray();
@ -2542,7 +2542,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
var result = _libraryManager.GetItemIds(new InternalItemsQuery var result = _libraryManager.GetItemIds(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Episode).Name }, IncludeItemTypes = new[] { nameof(Episode) },
ParentIndexNumber = program.SeasonNumber.Value, ParentIndexNumber = program.SeasonNumber.Value,
IndexNumber = program.EpisodeNumber.Value, IndexNumber = program.EpisodeNumber.Value,
AncestorIds = seriesIds, AncestorIds = seriesIds,

@ -15,6 +15,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
@ -33,17 +34,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost; private readonly IApplicationHost _appHost;
private readonly ICryptoProvider _cryptoProvider;
public SchedulesDirect( public SchedulesDirect(
ILogger<SchedulesDirect> logger, ILogger<SchedulesDirect> logger,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IApplicationHost appHost) IApplicationHost appHost,
ICryptoProvider cryptoProvider)
{ {
_logger = logger; _logger = logger;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_appHost = appHost; _appHost = appHost;
_cryptoProvider = cryptoProvider;
} }
private string UserAgent => _appHost.ApplicationUserAgent; private string UserAgent => _appHost.ApplicationUserAgent;
@ -642,7 +646,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json); var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
string hashedPassword = Hex.Encode(hashedPasswordBytes);
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(Series).Name }, IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName, Name = seriesName,
Limit = 1, Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb }, ImageTypes = new ImageType[] { ImageType.Thumb },
@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(Series).Name }, IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName, Name = seriesName,
Limit = 1, Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb }, ImageTypes = new ImageType[] { ImageType.Thumb },
@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv
var program = _libraryManager.GetItemList(new InternalItemsQuery var program = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(Series).Name }, IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName, Name = seriesName,
Limit = 1, Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary }, ImageTypes = new ImageType[] { ImageType.Primary },
@ -307,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
program = _libraryManager.GetItemList(new InternalItemsQuery program = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = programSeriesId, ExternalSeriesId = programSeriesId,
Limit = 1, Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary }, ImageTypes = new ImageType[] { ImageType.Primary },

@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv
IsKids = query.IsKids, IsKids = query.IsKids,
IsSports = query.IsSports, IsSports = query.IsSports,
IsSeries = query.IsSeries, IsSeries = query.IsSeries,
IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }, IncludeItemTypes = new[] { nameof(LiveTvChannel) },
TopParentIds = new[] { topFolder.Id }, TopParentIds = new[] { topFolder.Id },
IsFavorite = query.IsFavorite, IsFavorite = query.IsFavorite,
IsLiked = query.IsLiked, IsLiked = query.IsLiked,
@ -808,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user) var internalQuery = new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new[] { nameof(LiveTvProgram) },
MinEndDate = query.MinEndDate, MinEndDate = query.MinEndDate,
MinStartDate = query.MinStartDate, MinStartDate = query.MinStartDate,
MaxEndDate = query.MaxEndDate, MaxEndDate = query.MaxEndDate,
@ -872,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user) var internalQuery = new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new[] { nameof(LiveTvProgram) },
IsAiring = query.IsAiring, IsAiring = query.IsAiring,
HasAired = query.HasAired, HasAired = query.HasAired,
IsNews = query.IsNews, IsNews = query.IsNews,
@ -1089,8 +1089,8 @@ namespace Emby.Server.Implementations.LiveTv
if (cleanDatabase) if (cleanDatabase)
{ {
CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken); CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken); CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
} }
var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault(); var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
@ -1181,7 +1181,7 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ChannelIds = new Guid[] { currentChannel.Id }, ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id); }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
@ -1346,11 +1346,11 @@ namespace Emby.Server.Implementations.LiveTv
{ {
if (query.IsMovie.Value) if (query.IsMovie.Value)
{ {
includeItemTypes.Add(typeof(Movie).Name); includeItemTypes.Add(nameof(Movie));
} }
else else
{ {
excludeItemTypes.Add(typeof(Movie).Name); excludeItemTypes.Add(nameof(Movie));
} }
} }
@ -1358,11 +1358,11 @@ namespace Emby.Server.Implementations.LiveTv
{ {
if (query.IsSeries.Value) if (query.IsSeries.Value)
{ {
includeItemTypes.Add(typeof(Episode).Name); includeItemTypes.Add(nameof(Episode));
} }
else else
{ {
excludeItemTypes.Add(typeof(Episode).Name); excludeItemTypes.Add(nameof(Episode));
} }
} }
@ -1883,7 +1883,7 @@ namespace Emby.Server.Implementations.LiveTv
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new[] { nameof(LiveTvProgram) },
ChannelIds = channelIds, ChannelIds = channelIds,
MaxStartDate = now, MaxStartDate = now,
MinEndDate = now, MinEndDate = now,

@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv
return new[] return new[]
{ {
// Every so often // Every so often
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
}; };
} }

@ -131,6 +131,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false); await taskCompletionSource.Task.ConfigureAwait(false);
} }
public string GetFilePath()
{
return TempFilePath;
}
private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{ {
return Task.Run(async () => return Task.Run(async () =>

@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
var channelIdPrefix = GetFullChannelIdPrefix(info); var channelIdPrefix = GetFullChannelIdPrefix(info);
return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); return await new M3uParser(Logger, _httpClientFactory, _appHost)
.Parse(info, channelIdPrefix, cancellationToken)
.ConfigureAwait(false);
} }
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info) public async Task Validate(TunerHostInfo info)
{ {
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false)) using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{ {
} }
} }

@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts namespace Emby.Server.Implementations.LiveTv.TunerHosts
@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_appHost = appHost; _appHost = appHost;
} }
public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken) public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
{ {
// Read the file and display it line by line. // Read the file and display it line by line.
using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false))) using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
{ {
return GetChannels(reader, channelIdPrefix, tunerHostId); return GetChannels(reader, channelIdPrefix, info.Id);
} }
} }
@ -48,15 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
} }
} }
public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken) public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
{ {
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{ {
return _httpClientFactory.CreateClient(NamedClient.Default) using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
.GetStreamAsync(url); if (!string.IsNullOrEmpty(info.UserAgent))
{
requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
}
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(requestMessage, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
} }
return Task.FromResult((Stream)File.OpenRead(url)); return File.OpenRead(info.Url);
} }
private const string ExtInfPrefix = "#EXTINF:"; private const string ExtInfPrefix = "#EXTINF:";

@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var typeName = GetType().Name; var typeName = GetType().Name;
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url); Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) // Response stream is disposed manually.
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -121,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
} }
} }
public string GetFilePath()
{
return TempFilePath;
}
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{ {
return Task.Run(async () => return Task.Run(async () =>

@ -85,7 +85,6 @@
"ItemAddedWithName": "{0} is in die versameling", "ItemAddedWithName": "{0} is in die versameling",
"HomeVideos": "Tuis opnames", "HomeVideos": "Tuis opnames",
"HeaderRecordingGroups": "Groep Opnames", "HeaderRecordingGroups": "Groep Opnames",
"HeaderCameraUploads": "Kamera Oplaai",
"Genres": "Genres", "Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}", "FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
"ChapterNameValue": "Hoofstuk", "ChapterNameValue": "Hoofstuk",

@ -16,7 +16,6 @@
"Folders": "المجلدات", "Folders": "المجلدات",
"Genres": "التضنيفات", "Genres": "التضنيفات",
"HeaderAlbumArtists": "فناني الألبومات", "HeaderAlbumArtists": "فناني الألبومات",
"HeaderCameraUploads": "تحميلات الكاميرا",
"HeaderContinueWatching": "استئناف", "HeaderContinueWatching": "استئناف",
"HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteArtists": "الفنانون المفضلون",

@ -16,7 +16,6 @@
"Folders": "Папки", "Folders": "Папки",
"Genres": "Жанрове", "Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албуми", "HeaderAlbumArtists": "Изпълнители на албуми",
"HeaderCameraUploads": "Качени от камера",
"HeaderContinueWatching": "Продължаване на гледането", "HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми", "HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители", "HeaderFavoriteArtists": "Любими изпълнители",

@ -14,7 +14,6 @@
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন", "HeaderContinueWatching": "দেখতে থাকুন",
"HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
"HeaderAlbumArtists": "এলবাম শিল্পী", "HeaderAlbumArtists": "এলবাম শিল্পী",
"Genres": "জেনার", "Genres": "জেনার",
"Folders": "ফোল্ডারগুলো", "Folders": "ফোল্ডারগুলো",

@ -16,7 +16,6 @@
"Folders": "Carpetes", "Folders": "Carpetes",
"Genres": "Gèneres", "Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes del Àlbum", "HeaderAlbumArtists": "Artistes del Àlbum",
"HeaderCameraUploads": "Pujades de Càmera",
"HeaderContinueWatching": "Continua Veient", "HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits", "HeaderFavoriteAlbums": "Àlbums Preferits",
"HeaderFavoriteArtists": "Artistes Preferits", "HeaderFavoriteArtists": "Artistes Preferits",

@ -16,7 +16,6 @@
"Folders": "Složky", "Folders": "Složky",
"Genres": "Žánry", "Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba", "HeaderAlbumArtists": "Umělci alba",
"HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovat ve sledování", "HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba", "HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti", "HeaderFavoriteArtists": "Oblíbení interpreti",
@ -114,5 +113,7 @@
"TasksChannelsCategory": "Internetové kanály", "TasksChannelsCategory": "Internetové kanály",
"TasksApplicationCategory": "Aplikace", "TasksApplicationCategory": "Aplikace",
"TasksLibraryCategory": "Knihovna", "TasksLibraryCategory": "Knihovna",
"TasksMaintenanceCategory": "Údržba" "TasksMaintenanceCategory": "Údržba",
"TaskCleanActivityLogDescription": "Smazat záznamy o aktivitě, které jsou starší než zadaná doba.",
"TaskCleanActivityLog": "Smazat záznam aktivity"
} }

@ -16,7 +16,6 @@
"Folders": "Mapper", "Folders": "Mapper",
"Genres": "Genrer", "Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstnere", "HeaderAlbumArtists": "Albumkunstnere",
"HeaderCameraUploads": "Kamera Uploads",
"HeaderContinueWatching": "Fortsæt Afspilning", "HeaderContinueWatching": "Fortsæt Afspilning",
"HeaderFavoriteAlbums": "Favoritalbummer", "HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favoritkunstnere", "HeaderFavoriteArtists": "Favoritkunstnere",

@ -16,7 +16,6 @@
"Folders": "Verzeichnisse", "Folders": "Verzeichnisse",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Album-Interpreten", "HeaderAlbumArtists": "Album-Interpreten",
"HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "Fortsetzen", "HeaderContinueWatching": "Fortsetzen",
"HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Interpreten", "HeaderFavoriteArtists": "Lieblings-Interpreten",
@ -114,5 +113,7 @@
"TasksChannelsCategory": "Internet Kanäle", "TasksChannelsCategory": "Internet Kanäle",
"TasksApplicationCategory": "Anwendung", "TasksApplicationCategory": "Anwendung",
"TasksLibraryCategory": "Bibliothek", "TasksLibraryCategory": "Bibliothek",
"TasksMaintenanceCategory": "Wartung" "TasksMaintenanceCategory": "Wartung",
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
"TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen"
} }

@ -16,7 +16,6 @@
"Folders": "Φάκελοι", "Folders": "Φάκελοι",
"Genres": "Είδη", "Genres": "Είδη",
"HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ", "HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
"HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση", "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ", "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες", "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",

@ -16,7 +16,6 @@
"Folders": "Folders", "Folders": "Folders",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Album Artists", "HeaderAlbumArtists": "Album Artists",
"HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching", "HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums", "HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists", "HeaderFavoriteArtists": "Favourite Artists",
@ -114,5 +113,7 @@
"TasksChannelsCategory": "Internet Channels", "TasksChannelsCategory": "Internet Channels",
"TasksApplicationCategory": "Application", "TasksApplicationCategory": "Application",
"TasksLibraryCategory": "Library", "TasksLibraryCategory": "Library",
"TasksMaintenanceCategory": "Maintenance" "TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
"TaskCleanActivityLog": "Clean Activity Log"
} }

@ -16,7 +16,6 @@
"Folders": "Folders", "Folders": "Folders",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Album Artists", "HeaderAlbumArtists": "Album Artists",
"HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching", "HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists", "HeaderFavoriteArtists": "Favorite Artists",
@ -96,6 +95,8 @@
"TasksLibraryCategory": "Library", "TasksLibraryCategory": "Library",
"TasksApplicationCategory": "Application", "TasksApplicationCategory": "Application",
"TasksChannelsCategory": "Internet Channels", "TasksChannelsCategory": "Internet Channels",
"TaskCleanActivityLog": "Clean Activity Log",
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
"TaskCleanCache": "Clean Cache Directory", "TaskCleanCache": "Clean Cache Directory",
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.", "TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
"TaskRefreshChapterImages": "Extract Chapter Images", "TaskRefreshChapterImages": "Extract Chapter Images",

@ -16,7 +16,6 @@
"Folders": "Carpetas", "Folders": "Carpetas",
"Genres": "Géneros", "Genres": "Géneros",
"HeaderAlbumArtists": "Artistas de álbum", "HeaderAlbumArtists": "Artistas de álbum",
"HeaderCameraUploads": "Subidas de cámara",
"HeaderContinueWatching": "Seguir viendo", "HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteArtists": "Artistas favoritos",

@ -16,7 +16,6 @@
"Folders": "Carpetas", "Folders": "Carpetas",
"Genres": "Géneros", "Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum", "HeaderAlbumArtists": "Artistas del álbum",
"HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar viendo", "HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteArtists": "Artistas favoritos",

@ -16,7 +16,6 @@
"Folders": "Carpetas", "Folders": "Carpetas",
"Genres": "Géneros", "Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum", "HeaderAlbumArtists": "Artistas del álbum",
"HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar viendo", "HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteArtists": "Artistas favoritos",
@ -78,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}", "SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar", "Sync": "Sincronizar",
"System": "Sistema", "System": "Sistema",
"TvShows": "Programas de televisión", "TvShows": "Series",
"User": "Usuario", "User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado", "UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado", "UserDeletedWithName": "El usuario {0} ha sido borrado",
@ -114,5 +113,7 @@
"TaskRefreshChannels": "Actualizar canales", "TaskRefreshChannels": "Actualizar canales",
"TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.", "TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.",
"TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan", "TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan",
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos." "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos.",
"TaskCleanActivityLogDescription": "Elimina todos los registros de actividad anteriores a la fecha configurada.",
"TaskCleanActivityLog": "Limpiar registro de actividad"
} }

@ -105,7 +105,6 @@
"Inherit": "Heredar", "Inherit": "Heredar",
"HomeVideos": "Videos caseros", "HomeVideos": "Videos caseros",
"HeaderRecordingGroups": "Grupos de grabación", "HeaderRecordingGroups": "Grupos de grabación",
"HeaderCameraUploads": "Subidas desde la cámara",
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}", "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
"DeviceOnlineWithName": "{0} está conectado", "DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} se ha desconectado", "DeviceOfflineWithName": "{0} se ha desconectado",

@ -12,7 +12,6 @@
"Application": "Aplicación", "Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}", "AppDeviceValues": "App: {0}, Dispositivo: {1}",
"HeaderContinueWatching": "Continuar Viendo", "HeaderContinueWatching": "Continuar Viendo",
"HeaderCameraUploads": "Subidas de Cámara",
"HeaderAlbumArtists": "Artistas del Álbum", "HeaderAlbumArtists": "Artistas del Álbum",
"Genres": "Géneros", "Genres": "Géneros",
"Folders": "Carpetas", "Folders": "Carpetas",

@ -16,7 +16,6 @@
"Folders": "پوشه‌ها", "Folders": "پوشه‌ها",
"Genres": "ژانرها", "Genres": "ژانرها",
"HeaderAlbumArtists": "هنرمندان آلبوم", "HeaderAlbumArtists": "هنرمندان آلبوم",
"HeaderCameraUploads": "آپلودهای دوربین",
"HeaderContinueWatching": "ادامه تماشا", "HeaderContinueWatching": "ادامه تماشا",
"HeaderFavoriteAlbums": "آلبوم‌های مورد علاقه", "HeaderFavoriteAlbums": "آلبوم‌های مورد علاقه",
"HeaderFavoriteArtists": "هنرمندان مورد علاقه", "HeaderFavoriteArtists": "هنرمندان مورد علاقه",

@ -1,7 +1,7 @@
{ {
"HeaderLiveTV": "Live-TV", "HeaderLiveTV": "Live-TV",
"NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.", "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
"NameSeasonUnknown": "Tuntematon Kausi", "NameSeasonUnknown": "Tuntematon kausi",
"NameSeasonNumber": "Kausi {0}", "NameSeasonNumber": "Kausi {0}",
"NameInstallFailed": "{0} asennus epäonnistui", "NameInstallFailed": "{0} asennus epäonnistui",
"MusicVideos": "Musiikkivideot", "MusicVideos": "Musiikkivideot",
@ -19,24 +19,23 @@
"ItemAddedWithName": "{0} lisättiin kirjastoon", "ItemAddedWithName": "{0} lisättiin kirjastoon",
"Inherit": "Periytyä", "Inherit": "Periytyä",
"HomeVideos": "Kotivideot", "HomeVideos": "Kotivideot",
"HeaderRecordingGroups": "Nauhoiteryhmät", "HeaderRecordingGroups": "Tallennusryhmät",
"HeaderNextUp": "Seuraavaksi", "HeaderNextUp": "Seuraavaksi",
"HeaderFavoriteSongs": "Lempikappaleet", "HeaderFavoriteSongs": "Suosikkikappaleet",
"HeaderFavoriteShows": "Lempisarjat", "HeaderFavoriteShows": "Suosikkisarjat",
"HeaderFavoriteEpisodes": "Lempijaksot", "HeaderFavoriteEpisodes": "Suosikkijaksot",
"HeaderCameraUploads": "Kamerasta Lähetetyt", "HeaderFavoriteArtists": "Suosikkiartistit",
"HeaderFavoriteArtists": "Lempiartistit", "HeaderFavoriteAlbums": "Suosikkialbumit",
"HeaderFavoriteAlbums": "Lempialbumit",
"HeaderContinueWatching": "Jatka katsomista", "HeaderContinueWatching": "Jatka katsomista",
"HeaderAlbumArtists": "Albumin esittäjä", "HeaderAlbumArtists": "Albumin artistit",
"Genres": "Tyylilajit", "Genres": "Tyylilajit",
"Folders": "Kansiot", "Folders": "Kansiot",
"Favorites": "Suosikit", "Favorites": "Suosikit",
"FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}", "FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
"DeviceOnlineWithName": "{0} on yhdistetty", "DeviceOnlineWithName": "{0} on yhdistetty",
"DeviceOfflineWithName": "{0} on katkaissut yhteytensä", "DeviceOfflineWithName": "{0} yhteys on katkaistu",
"Collections": "Kokoelmat", "Collections": "Kokoelmat",
"ChapterNameValue": "Luku: {0}", "ChapterNameValue": "Jakso: {0}",
"Channels": "Kanavat", "Channels": "Kanavat",
"CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}", "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
"Books": "Kirjat", "Books": "Kirjat",
@ -62,25 +61,25 @@
"UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}", "UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}",
"UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}", "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
"UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}", "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
"UserOfflineFromDevice": "{0} yhteys katkaistu {1}", "UserOfflineFromDevice": "{0} yhteys katkaistu kohteesta {1}",
"UserLockedOutWithName": "Käyttäjä {0} lukittu", "UserLockedOutWithName": "Käyttäjä {0} lukittu",
"UserDownloadingItemWithValues": "{0} lataa {1}", "UserDownloadingItemWithValues": "{0} lataa {1}",
"UserDeletedWithName": "Käyttäjä {0} poistettu", "UserDeletedWithName": "Käyttäjä {0} poistettu",
"UserCreatedWithName": "Käyttäjä {0} luotu", "UserCreatedWithName": "Käyttäjä {0} luotu",
"TvShows": "TV-sarjat", "TvShows": "TV-ohjelmat",
"Sync": "Synkronoi", "Sync": "Synkronoi",
"SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry", "SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}",
"StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.", "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.",
"Songs": "Kappaleet", "Songs": "Kappaleet",
"Shows": "Sarjat", "Shows": "Ohjelmat",
"ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen", "ServerNameNeedsToBeRestarted": "{0} on käynnistettävä uudelleen",
"ProviderValue": "Tarjoaja: {0}", "ProviderValue": "Tarjoaja: {0}",
"Plugin": "Liitännäinen", "Plugin": "Liitännäinen",
"NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty", "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
"NotificationOptionVideoPlayback": "Videota toistetaan", "NotificationOptionVideoPlayback": "Videota toistetaan",
"NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos", "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
"NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui", "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
"NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen", "NotificationOptionServerRestartRequired": "Palvelin on käynnistettävä uudelleen",
"NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty", "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
"NotificationOptionPluginUninstalled": "Liitännäinen poistettu", "NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
"NotificationOptionPluginInstalled": "Liitännäinen asennettu", "NotificationOptionPluginInstalled": "Liitännäinen asennettu",
@ -105,10 +104,10 @@
"TaskRefreshPeople": "Päivitä henkilöt", "TaskRefreshPeople": "Päivitä henkilöt",
"TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.", "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
"TaskCleanLogs": "Puhdista lokihakemisto", "TaskCleanLogs": "Puhdista lokihakemisto",
"TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.", "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.",
"TaskRefreshLibrary": "Skannaa mediakirjasto", "TaskRefreshLibrary": "Skannaa mediakirjasto",
"TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.", "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on jaksoja.",
"TaskRefreshChapterImages": "Eristä lukujen kuvat", "TaskRefreshChapterImages": "Pura jakson kuvat",
"TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.", "TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
"TaskCleanCache": "Tyhjennä välimuisti-hakemisto", "TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
"TasksChannelsCategory": "Internet kanavat", "TasksChannelsCategory": "Internet kanavat",

@ -1,7 +1,7 @@
{ {
"VersionNumber": "Bersyon {0}", "VersionNumber": "Bersyon {0}",
"ValueSpecialEpisodeName": "Espesyal - {0}", "ValueSpecialEpisodeName": "Espesyal - {0}",
"ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong media library", "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
"UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}", "UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
"UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}", "UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
"UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}", "UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
@ -61,8 +61,8 @@
"Latest": "Pinakabago", "Latest": "Pinakabago",
"LabelRunningTimeValue": "Oras: {0}", "LabelRunningTimeValue": "Oras: {0}",
"LabelIpAddressValue": "Ang IP Address ay {0}", "LabelIpAddressValue": "Ang IP Address ay {0}",
"ItemRemovedWithName": "Naitanggal ang {0} sa library", "ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
"ItemAddedWithName": "Naidagdag ang {0} sa library", "ItemAddedWithName": "Naidagdag ang {0} sa librerya",
"Inherit": "Manahin", "Inherit": "Manahin",
"HeaderRecordingGroups": "Pagtatalang Grupo", "HeaderRecordingGroups": "Pagtatalang Grupo",
"HeaderNextUp": "Susunod", "HeaderNextUp": "Susunod",
@ -73,7 +73,6 @@
"HeaderFavoriteArtists": "Paboritong Artista", "HeaderFavoriteArtists": "Paboritong Artista",
"HeaderFavoriteAlbums": "Paboritong Albums", "HeaderFavoriteAlbums": "Paboritong Albums",
"HeaderContinueWatching": "Ituloy Manood", "HeaderContinueWatching": "Ituloy Manood",
"HeaderCameraUploads": "Camera Uploads",
"HeaderAlbumArtists": "Artista ng Album", "HeaderAlbumArtists": "Artista ng Album",
"Genres": "Kategorya", "Genres": "Kategorya",
"Folders": "Folders", "Folders": "Folders",
@ -91,12 +90,29 @@
"Application": "Aplikasyon", "Application": "Aplikasyon",
"AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}", "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
"Albums": "Albums", "Albums": "Albums",
"TaskRefreshLibrary": "Suriin ang nasa librerya", "TaskRefreshLibrary": "Suriin and Librerya ng Medya",
"TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata", "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
"TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata", "TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
"TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.", "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
"TasksChannelsCategory": "Palabas sa internet", "TasksChannelsCategory": "Palabas sa internet",
"TasksLibraryCategory": "Librerya", "TasksLibraryCategory": "Librerya",
"TasksMaintenanceCategory": "Pagpapanatili", "TasksMaintenanceCategory": "Pagpapanatili",
"HomeVideos": "Sariling pelikula" "HomeVideos": "Sariling pelikula",
"TaskRefreshPeopleDescription": "Ini-update ang metadata para sa mga aktor at direktor sa iyong librerya ng medya.",
"TaskRefreshPeople": "I-refresh ang Tauhan",
"TaskDownloadMissingSubtitlesDescription": "Hinahanap sa internet ang mga nawawalang subtiles base sa metadata configuration.",
"TaskDownloadMissingSubtitles": "I-download and nawawalang subtitles",
"TaskRefreshChannelsDescription": "Ni-rerefresh ang impormasyon sa internet channels.",
"TaskRefreshChannels": "I-refresh ang Channels",
"TaskCleanTranscodeDescription": "Binubura ang transcode files na mas matanda ng isang araw.",
"TaskUpdatePluginsDescription": "Nag download at install ng updates sa plugins na naka configure para sa automatikong pag update.",
"TaskUpdatePlugins": "I-update ang Plugins",
"TaskCleanLogsDescription": "Binubura and files ng talaan na mas mantanda ng {0} araw.",
"TaskCleanTranscode": "Linisin and Direktoryo ng Transcode",
"TaskCleanLogs": "Linisin and Direktoryo ng Talaan",
"TaskRefreshLibraryDescription": "Sinusuri ang iyong librerya ng medya para sa bagong files at irefresh ang metadata.",
"TaskCleanCache": "Linisin and Direktoryo ng Cache",
"TasksApplicationCategory": "Application",
"TaskCleanActivityLog": "Linisin ang Tala ng Aktibidad",
"TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas matanda sa naka configure na edad."
} }

@ -16,7 +16,6 @@
"Folders": "Dossiers", "Folders": "Dossiers",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album", "HeaderAlbumArtists": "Artistes de l'album",
"HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder", "HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes favoris", "HeaderFavoriteArtists": "Artistes favoris",

@ -16,7 +16,6 @@
"Folders": "Dossiers", "Folders": "Dossiers",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Artistes", "HeaderAlbumArtists": "Artistes",
"HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder", "HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés", "HeaderFavoriteArtists": "Artistes préférés",
@ -114,5 +113,7 @@
"TaskCleanCache": "Vider le répertoire cache", "TaskCleanCache": "Vider le répertoire cache",
"TasksApplicationCategory": "Application", "TasksApplicationCategory": "Application",
"TasksLibraryCategory": "Bibliothèque", "TasksLibraryCategory": "Bibliothèque",
"TasksMaintenanceCategory": "Maintenance" "TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
"TaskCleanActivityLog": "Nettoyer le journal d'activité"
} }

@ -16,7 +16,6 @@
"Folders": "Ordner", "Folders": "Ordner",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Album-Künstler", "HeaderAlbumArtists": "Album-Künstler",
"HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "weiter schauen", "HeaderContinueWatching": "weiter schauen",
"HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Künstler", "HeaderFavoriteArtists": "Lieblings-Künstler",

@ -16,7 +16,6 @@
"Folders": "תיקיות", "Folders": "תיקיות",
"Genres": "ז'אנרים", "Genres": "ז'אנרים",
"HeaderAlbumArtists": "אמני האלבום", "HeaderAlbumArtists": "אמני האלבום",
"HeaderCameraUploads": "העלאות ממצלמה",
"HeaderContinueWatching": "המשך לצפות", "HeaderContinueWatching": "המשך לצפות",
"HeaderFavoriteAlbums": "אלבומים מועדפים", "HeaderFavoriteAlbums": "אלבומים מועדפים",
"HeaderFavoriteArtists": "אמנים מועדפים", "HeaderFavoriteArtists": "אמנים מועדפים",

@ -0,0 +1,3 @@
{
"Albums": "आल्बुम्"
}

@ -5,18 +5,17 @@
"Artists": "Izvođači", "Artists": "Izvođači",
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena", "AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
"Books": "Knjige", "Books": "Knjige",
"CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}", "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
"Channels": "Kanali", "Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}", "ChapterNameValue": "Poglavlje {0}",
"Collections": "Kolekcije", "Collections": "Kolekcije",
"DeviceOfflineWithName": "{0} se odspojilo", "DeviceOfflineWithName": "{0} je prekinuo vezu",
"DeviceOnlineWithName": "{0} je spojeno", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}", "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
"Favorites": "Favoriti", "Favorites": "Favoriti",
"Folders": "Mape", "Folders": "Mape",
"Genres": "Žanrovi", "Genres": "Žanrovi",
"HeaderAlbumArtists": "Izvođači na albumu", "HeaderAlbumArtists": "Izvođači na albumu",
"HeaderCameraUploads": "Uvoz sa kamere",
"HeaderContinueWatching": "Nastavi gledati", "HeaderContinueWatching": "Nastavi gledati",
"HeaderFavoriteAlbums": "Omiljeni albumi", "HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni izvođači", "HeaderFavoriteArtists": "Omiljeni izvođači",
@ -24,95 +23,97 @@
"HeaderFavoriteShows": "Omiljene serije", "HeaderFavoriteShows": "Omiljene serije",
"HeaderFavoriteSongs": "Omiljene pjesme", "HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo", "HeaderLiveTV": "TV uživo",
"HeaderNextUp": "Sljedeće je", "HeaderNextUp": "Slijedi",
"HeaderRecordingGroups": "Grupa snimka", "HeaderRecordingGroups": "Grupa snimka",
"HomeVideos": "Kućni videi", "HomeVideos": "Kućni video",
"Inherit": "Naslijedi", "Inherit": "Naslijedi",
"ItemAddedWithName": "{0} je dodano u biblioteku", "ItemAddedWithName": "{0} je dodano u biblioteku",
"ItemRemovedWithName": "{0} je uklonjen iz biblioteke", "ItemRemovedWithName": "{0} je uklonjeno iz biblioteke",
"LabelIpAddressValue": "IP adresa: {0}", "LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Vrijeme rada: {0}", "LabelRunningTimeValue": "Vrijeme rada: {0}",
"Latest": "Najnovije", "Latest": "Najnovije",
"MessageApplicationUpdated": "Jellyfin Server je ažuriran", "MessageApplicationUpdated": "Jellyfin server je ažuriran",
"MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}", "MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran", "MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran",
"MessageServerConfigurationUpdated": "Postavke servera su ažurirane", "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
"MixedContent": "Miješani sadržaj", "MixedContent": "Miješani sadržaj",
"Movies": "Filmovi", "Movies": "Filmovi",
"Music": "Glazba", "Music": "Glazba",
"MusicVideos": "Glazbeni spotovi", "MusicVideos": "Glazbeni spotovi",
"NameInstallFailed": "{0} neuspješnih instalacija", "NameInstallFailed": "{0} neuspješnih instalacija",
"NameSeasonNumber": "Sezona {0}", "NameSeasonNumber": "Sezona {0}",
"NameSeasonUnknown": "Nepoznata sezona", "NameSeasonUnknown": "Sezona nepoznata",
"NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.", "NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije", "NotificationOptionApplicationUpdateAvailable": "Dostupno je ažuriranje aplikacije",
"NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije", "NotificationOptionApplicationUpdateInstalled": "Instalirano je ažuriranje aplikacije",
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta", "NotificationOptionAudioPlayback": "Reprodukcija glazbe započela",
"NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena", "NotificationOptionAudioPlaybackStopped": "Reprodukcija glazbe zaustavljena",
"NotificationOptionCameraImageUploaded": "Slike kamere preuzete", "NotificationOptionCameraImageUploaded": "Slika s kamere učitana",
"NotificationOptionInstallationFailed": "Instalacija neuspješna", "NotificationOptionInstallationFailed": "Instalacija nije uspjela",
"NotificationOptionNewLibraryContent": "Novi sadržaj je dodan", "NotificationOptionNewLibraryContent": "Novi sadržaj dodan",
"NotificationOptionPluginError": "Dodatak otkazao", "NotificationOptionPluginError": "Dodatak zakazao",
"NotificationOptionPluginInstalled": "Dodatak instaliran", "NotificationOptionPluginInstalled": "Dodatak instaliran",
"NotificationOptionPluginUninstalled": "Dodatak uklonjen", "NotificationOptionPluginUninstalled": "Dodatak deinstaliran",
"NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje za dodatak", "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje dodatka",
"NotificationOptionServerRestartRequired": "Potrebno ponovo pokretanje servera", "NotificationOptionServerRestartRequired": "Ponovno pokrenite server",
"NotificationOptionTaskFailed": "Zakazan zadatak nije izvršen", "NotificationOptionTaskFailed": "Greška zakazanog zadatka",
"NotificationOptionUserLockedOut": "Korisnik zaključan", "NotificationOptionUserLockedOut": "Korisnik zaključan",
"NotificationOptionVideoPlayback": "Reprodukcija videa započeta", "NotificationOptionVideoPlayback": "Reprodukcija videa započela",
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena", "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena",
"Photos": "Slike", "Photos": "Fotografije",
"Playlists": "Popis za reprodukciju", "Playlists": "Popisi za reprodukciju",
"Plugin": "Dodatak", "Plugin": "Dodatak",
"PluginInstalledWithName": "{0} je instalirano", "PluginInstalledWithName": "{0} je instalirano",
"PluginUninstalledWithName": "{0} je deinstalirano", "PluginUninstalledWithName": "{0} je deinstalirano",
"PluginUpdatedWithName": "{0} je ažurirano", "PluginUpdatedWithName": "{0} je ažurirano",
"ProviderValue": "Pružitelj: {0}", "ProviderValue": "Pružatelj: {0}",
"ScheduledTaskFailedWithName": "{0} neuspjelo", "ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto", "ScheduledTaskStartedWithName": "{0} pokrenuto",
"ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto", "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Serije", "Shows": "Serije",
"Songs": "Pjesme", "Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.", "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
"SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}", "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
"SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}", "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
"Sync": "Sink.", "Sync": "Sinkronizacija",
"System": "Sistem", "System": "Sustav",
"TvShows": "Serije", "TvShows": "Serije",
"User": "Korisnik", "User": "Korisnik",
"UserCreatedWithName": "Korisnik {0} je stvoren", "UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je obrisan", "UserDeletedWithName": "Korisnik {0} je obrisan",
"UserDownloadingItemWithValues": "{0} se preuzima {1}", "UserDownloadingItemWithValues": "{0} preuzima {1}",
"UserLockedOutWithName": "Korisnik {0} je zaključan", "UserLockedOutWithName": "Korisnik {0} je zaključan",
"UserOfflineFromDevice": "{0} se odspojilo od {1}", "UserOfflineFromDevice": "{0} prekinuo vezu od {1}",
"UserOnlineFromDevice": "{0} je online od {1}", "UserOnlineFromDevice": "{0} povezan od {1}",
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}", "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
"UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}", "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
"UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}", "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}", "UserStoppedPlayingItemWithValues": "{0} je zavio reprodukciju {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku", "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
"ValueSpecialEpisodeName": "Specijal - {0}", "ValueSpecialEpisodeName": "Posebno - {0}",
"VersionNumber": "Verzija {0}", "VersionNumber": "Verzija {0}",
"TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.", "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
"TaskRefreshLibrary": "Skeniraj medijsku knjižnicu", "TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
"TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.", "TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
"TaskRefreshChapterImages": "Raspakiraj slike poglavlja", "TaskRefreshChapterImages": "Izdvoji slike poglavlja",
"TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.", "TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
"TaskCleanCache": "Očisti priručnu memoriju", "TaskCleanCache": "Očisti mapu predmemorije",
"TasksApplicationCategory": "Aplikacija", "TasksApplicationCategory": "Aplikacija",
"TasksMaintenanceCategory": "Održavanje", "TasksMaintenanceCategory": "Održavanje",
"TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.", "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
"TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju", "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
"TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.", "TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
"TaskRefreshChannels": "Osvježi kanale", "TaskRefreshChannels": "Osvježi kanale",
"TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.", "TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
"TaskCleanTranscode": "Očisti direktorij za transkodiranje", "TaskCleanTranscode": "Očisti mapu transkodiranja",
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.", "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
"TaskUpdatePlugins": "Ažuriraj dodatke", "TaskUpdatePlugins": "Ažuriraj dodatke",
"TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.", "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
"TaskRefreshPeople": "Osvježi ljude", "TaskRefreshPeople": "Osvježi osobe",
"TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.", "TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
"TaskCleanLogs": "Očisti direktorij sa logovima", "TaskCleanLogs": "Očisti mapu dnevnika zapisa",
"TasksChannelsCategory": "Internet kanali", "TasksChannelsCategory": "Internet kanali",
"TasksLibraryCategory": "Biblioteka" "TasksLibraryCategory": "Biblioteka",
"TaskCleanActivityLogDescription": "Briše zapise dnevnika aktivnosti starije od navedenog vremena.",
"TaskCleanActivityLog": "Očisti dnevnik aktivnosti"
} }

@ -16,7 +16,6 @@
"Folders": "Könyvtárak", "Folders": "Könyvtárak",
"Genres": "Műfajok", "Genres": "Műfajok",
"HeaderAlbumArtists": "Album előadók", "HeaderAlbumArtists": "Album előadók",
"HeaderCameraUploads": "Kamera feltöltések",
"HeaderContinueWatching": "Megtekintés folytatása", "HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok", "HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók", "HeaderFavoriteArtists": "Kedvenc előadók",

@ -20,7 +20,6 @@
"HeaderFavoriteArtists": "Artis Favorit", "HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit", "HeaderFavoriteAlbums": "Album Favorit",
"HeaderContinueWatching": "Lanjut Menonton", "HeaderContinueWatching": "Lanjut Menonton",
"HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis", "HeaderAlbumArtists": "Album Artis",
"Genres": "Aliran", "Genres": "Aliran",
"Folders": "Folder", "Folders": "Folder",

@ -13,7 +13,6 @@
"HeaderFavoriteArtists": "Uppáhalds Listamenn", "HeaderFavoriteArtists": "Uppáhalds Listamenn",
"HeaderFavoriteAlbums": "Uppáhalds Plötur", "HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa", "HeaderContinueWatching": "Halda áfram að horfa",
"HeaderCameraUploads": "Myndavéla upphal",
"HeaderAlbumArtists": "Höfundur plötu", "HeaderAlbumArtists": "Höfundur plötu",
"Genres": "Tegundir", "Genres": "Tegundir",
"Folders": "Möppur", "Folders": "Möppur",

@ -16,7 +16,6 @@
"Folders": "Cartelle", "Folders": "Cartelle",
"Genres": "Generi", "Genres": "Generi",
"HeaderAlbumArtists": "Artisti degli Album", "HeaderAlbumArtists": "Artisti degli Album",
"HeaderCameraUploads": "Caricamenti Fotocamera",
"HeaderContinueWatching": "Continua a guardare", "HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti", "HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti", "HeaderFavoriteArtists": "Artisti Preferiti",
@ -114,5 +113,7 @@
"TasksChannelsCategory": "Canali su Internet", "TasksChannelsCategory": "Canali su Internet",
"TasksApplicationCategory": "Applicazione", "TasksApplicationCategory": "Applicazione",
"TasksLibraryCategory": "Libreria", "TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione" "TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate",
"TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie delletà configurata."
} }

@ -16,7 +16,6 @@
"Folders": "フォルダー", "Folders": "フォルダー",
"Genres": "ジャンル", "Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト", "HeaderAlbumArtists": "アルバムアーティスト",
"HeaderCameraUploads": "カメラアップロード",
"HeaderContinueWatching": "視聴を続ける", "HeaderContinueWatching": "視聴を続ける",
"HeaderFavoriteAlbums": "お気に入りのアルバム", "HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト", "HeaderFavoriteArtists": "お気に入りのアーティスト",
@ -97,7 +96,7 @@
"TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。", "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。",
"TaskRefreshLibrary": "メディアライブラリのスキャン", "TaskRefreshLibrary": "メディアライブラリのスキャン",
"TaskCleanCacheDescription": "不要なキャッシュを消去します。", "TaskCleanCacheDescription": "不要なキャッシュを消去します。",
"TaskCleanCache": "キャッシュの掃除", "TaskCleanCache": "キャッシュを消去",
"TasksChannelsCategory": "ネットチャンネル", "TasksChannelsCategory": "ネットチャンネル",
"TasksApplicationCategory": "アプリケーション", "TasksApplicationCategory": "アプリケーション",
"TasksLibraryCategory": "ライブラリ", "TasksLibraryCategory": "ライブラリ",
@ -113,5 +112,7 @@
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。", "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。", "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
"TaskRefreshChapterImages": "チャプター画像を抽出する", "TaskRefreshChapterImages": "チャプター画像を抽出する",
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする" "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
"TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
"TaskCleanActivityLog": "アクティビティの履歴を消去"
} }

@ -16,7 +16,6 @@
"Folders": "Qaltalar", "Folders": "Qaltalar",
"Genres": "Janrlar", "Genres": "Janrlar",
"HeaderAlbumArtists": "Álbom oryndaýshylary", "HeaderAlbumArtists": "Álbom oryndaýshylary",
"HeaderCameraUploads": "Kameradan júktelgender",
"HeaderContinueWatching": "Qaraýdy jalǵastyrý", "HeaderContinueWatching": "Qaraýdy jalǵastyrý",
"HeaderFavoriteAlbums": "Tańdaýly álbomdar", "HeaderFavoriteAlbums": "Tańdaýly álbomdar",
"HeaderFavoriteArtists": "Tańdaýly oryndaýshylar", "HeaderFavoriteArtists": "Tańdaýly oryndaýshylar",

@ -16,7 +16,6 @@
"Folders": "폴더", "Folders": "폴더",
"Genres": "장르", "Genres": "장르",
"HeaderAlbumArtists": "앨범 아티스트", "HeaderAlbumArtists": "앨범 아티스트",
"HeaderCameraUploads": "카메라 업로드",
"HeaderContinueWatching": "계속 시청하기", "HeaderContinueWatching": "계속 시청하기",
"HeaderFavoriteAlbums": "즐겨찾는 앨범", "HeaderFavoriteAlbums": "즐겨찾는 앨범",
"HeaderFavoriteArtists": "즐겨찾는 아티스트", "HeaderFavoriteArtists": "즐겨찾는 아티스트",
@ -28,7 +27,7 @@
"HeaderRecordingGroups": "녹화 그룹", "HeaderRecordingGroups": "녹화 그룹",
"HomeVideos": "홈 비디오", "HomeVideos": "홈 비디오",
"Inherit": "상속", "Inherit": "상속",
"ItemAddedWithName": "{0}가 라이브러리에 추가", "ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다",
"ItemRemovedWithName": "{0}가 라이브러리에서 제거됨", "ItemRemovedWithName": "{0}가 라이브러리에서 제거됨",
"LabelIpAddressValue": "IP 주소: {0}", "LabelIpAddressValue": "IP 주소: {0}",
"LabelRunningTimeValue": "상영 시간: {0}", "LabelRunningTimeValue": "상영 시간: {0}",
@ -114,5 +113,7 @@
"TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.", "TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.",
"TaskCleanCache": "캐시 폴더 청소", "TaskCleanCache": "캐시 폴더 청소",
"TasksChannelsCategory": "인터넷 채널", "TasksChannelsCategory": "인터넷 채널",
"TasksLibraryCategory": "라이브러리" "TasksLibraryCategory": "라이브러리",
"TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.",
"TaskCleanActivityLog": "활동내역청소"
} }

@ -16,7 +16,6 @@
"Folders": "Katalogai", "Folders": "Katalogai",
"Genres": "Žanrai", "Genres": "Žanrai",
"HeaderAlbumArtists": "Albumo atlikėjai", "HeaderAlbumArtists": "Albumo atlikėjai",
"HeaderCameraUploads": "Kameros",
"HeaderContinueWatching": "Žiūrėti toliau", "HeaderContinueWatching": "Žiūrėti toliau",
"HeaderFavoriteAlbums": "Mėgstami Albumai", "HeaderFavoriteAlbums": "Mėgstami Albumai",
"HeaderFavoriteArtists": "Mėgstami Atlikėjai", "HeaderFavoriteArtists": "Mėgstami Atlikėjai",

@ -72,7 +72,6 @@
"ItemAddedWithName": "{0} tika pievienots bibliotēkai", "ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV", "HeaderLiveTV": "Tiešraides TV",
"HeaderContinueWatching": "Turpināt Skatīšanos", "HeaderContinueWatching": "Turpināt Skatīšanos",
"HeaderCameraUploads": "Kameras augšupielādes",
"HeaderAlbumArtists": "Albumu Izpildītāji", "HeaderAlbumArtists": "Albumu Izpildītāji",
"Genres": "Žanri", "Genres": "Žanri",
"Folders": "Mapes", "Folders": "Mapes",

@ -51,7 +51,6 @@
"HeaderFavoriteArtists": "Омилени Изведувачи", "HeaderFavoriteArtists": "Омилени Изведувачи",
"HeaderFavoriteAlbums": "Омилени Албуми", "HeaderFavoriteAlbums": "Омилени Албуми",
"HeaderContinueWatching": "Продолжи со гледање", "HeaderContinueWatching": "Продолжи со гледање",
"HeaderCameraUploads": "Поставувања од камера",
"HeaderAlbumArtists": "Изведувачи од Албуми", "HeaderAlbumArtists": "Изведувачи од Албуми",
"Genres": "Жанрови", "Genres": "Жанрови",
"Folders": "Папки", "Folders": "Папки",

@ -54,7 +54,6 @@
"ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले", "ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले",
"HomeVideos": "घरचे व्हिडीयो", "HomeVideos": "घरचे व्हिडीयो",
"HeaderRecordingGroups": "रेकॉर्डिंग गट", "HeaderRecordingGroups": "रेकॉर्डिंग गट",
"HeaderCameraUploads": "कॅमेरा अपलोड",
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे", "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
"Application": "अ‍ॅप्लिकेशन", "Application": "अ‍ॅप्लिकेशन",
"AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}", "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",

@ -16,7 +16,6 @@
"Folders": "Fail-fail", "Folders": "Fail-fail",
"Genres": "Genre-genre", "Genres": "Genre-genre",
"HeaderAlbumArtists": "Album Artis-artis", "HeaderAlbumArtists": "Album Artis-artis",
"HeaderCameraUploads": "Muatnaik Kamera",
"HeaderContinueWatching": "Terus Menonton", "HeaderContinueWatching": "Terus Menonton",
"HeaderFavoriteAlbums": "Album-album Kegemaran", "HeaderFavoriteAlbums": "Album-album Kegemaran",
"HeaderFavoriteArtists": "Artis-artis Kegemaran", "HeaderFavoriteArtists": "Artis-artis Kegemaran",

@ -16,7 +16,6 @@
"Folders": "Mapper", "Folders": "Mapper",
"Genres": "Sjangre", "Genres": "Sjangre",
"HeaderAlbumArtists": "Albumartister", "HeaderAlbumArtists": "Albumartister",
"HeaderCameraUploads": "Kameraopplastinger",
"HeaderContinueWatching": "Fortsett å se", "HeaderContinueWatching": "Fortsett å se",
"HeaderFavoriteAlbums": "Favorittalbum", "HeaderFavoriteAlbums": "Favorittalbum",
"HeaderFavoriteArtists": "Favorittartister", "HeaderFavoriteArtists": "Favorittartister",

@ -41,7 +41,6 @@
"HeaderFavoriteArtists": "मनपर्ने कलाकारहरू", "HeaderFavoriteArtists": "मनपर्ने कलाकारहरू",
"HeaderFavoriteAlbums": "मनपर्ने एल्बमहरू", "HeaderFavoriteAlbums": "मनपर्ने एल्बमहरू",
"HeaderContinueWatching": "हेर्न जारी राख्नुहोस्", "HeaderContinueWatching": "हेर्न जारी राख्नुहोस्",
"HeaderCameraUploads": "क्यामेरा अपलोडहरू",
"HeaderAlbumArtists": "एल्बमका कलाकारहरू", "HeaderAlbumArtists": "एल्बमका कलाकारहरू",
"Genres": "विधाहरू", "Genres": "विधाहरू",
"Folders": "फोल्डरहरू", "Folders": "फोल्डरहरू",

@ -16,7 +16,6 @@
"Folders": "Mappen", "Folders": "Mappen",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Albumartiesten", "HeaderAlbumArtists": "Albumartiesten",
"HeaderCameraUploads": "Camera-uploads",
"HeaderContinueWatching": "Kijken hervatten", "HeaderContinueWatching": "Kijken hervatten",
"HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten", "HeaderFavoriteArtists": "Favoriete artiesten",
@ -114,5 +113,7 @@
"TasksChannelsCategory": "Internet Kanalen", "TasksChannelsCategory": "Internet Kanalen",
"TasksApplicationCategory": "Applicatie", "TasksApplicationCategory": "Applicatie",
"TasksLibraryCategory": "Bibliotheek", "TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud" "TasksMaintenanceCategory": "Onderhoud",
"TaskCleanActivityLogDescription": "Verwijder activiteiten logs ouder dan de ingestelde tijd.",
"TaskCleanActivityLog": "Leeg activiteiten logboek"
} }

@ -19,7 +19,6 @@
"HeaderFavoriteArtists": "Favoritt Artistar", "HeaderFavoriteArtists": "Favoritt Artistar",
"HeaderFavoriteAlbums": "Favoritt Album", "HeaderFavoriteAlbums": "Favoritt Album",
"HeaderContinueWatching": "Fortsett å sjå", "HeaderContinueWatching": "Fortsett å sjå",
"HeaderCameraUploads": "Kamera Opplastingar",
"HeaderAlbumArtists": "Album Artist", "HeaderAlbumArtists": "Album Artist",
"Genres": "Sjangrar", "Genres": "Sjangrar",
"Folders": "Mapper", "Folders": "Mapper",

@ -16,7 +16,6 @@
"Folders": "Foldery", "Folders": "Foldery",
"Genres": "Gatunki", "Genres": "Gatunki",
"HeaderAlbumArtists": "Wykonawcy albumów", "HeaderAlbumArtists": "Wykonawcy albumów",
"HeaderCameraUploads": "Przekazane obrazy",
"HeaderContinueWatching": "Kontynuuj odtwarzanie", "HeaderContinueWatching": "Kontynuuj odtwarzanie",
"HeaderFavoriteAlbums": "Ulubione albumy", "HeaderFavoriteAlbums": "Ulubione albumy",
"HeaderFavoriteArtists": "Ulubieni wykonawcy", "HeaderFavoriteArtists": "Ulubieni wykonawcy",

@ -16,7 +16,6 @@
"Folders": "Pastas", "Folders": "Pastas",
"Genres": "Gêneros", "Genres": "Gêneros",
"HeaderAlbumArtists": "Artistas do Álbum", "HeaderAlbumArtists": "Artistas do Álbum",
"HeaderCameraUploads": "Envios da Câmera",
"HeaderContinueWatching": "Continuar Assistindo", "HeaderContinueWatching": "Continuar Assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteArtists": "Artistas favoritos",

@ -16,7 +16,6 @@
"Folders": "Pastas", "Folders": "Pastas",
"Genres": "Géneros", "Genres": "Géneros",
"HeaderAlbumArtists": "Artistas do Álbum", "HeaderAlbumArtists": "Artistas do Álbum",
"HeaderCameraUploads": "Envios a partir da câmara",
"HeaderContinueWatching": "Continuar a Ver", "HeaderContinueWatching": "Continuar a Ver",
"HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos",
@ -26,7 +25,7 @@
"HeaderLiveTV": "TV em Direto", "HeaderLiveTV": "TV em Direto",
"HeaderNextUp": "A Seguir", "HeaderNextUp": "A Seguir",
"HeaderRecordingGroups": "Grupos de Gravação", "HeaderRecordingGroups": "Grupos de Gravação",
"HomeVideos": "Videos caseiros", "HomeVideos": "Vídeos Caseiros",
"Inherit": "Herdar", "Inherit": "Herdar",
"ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemAddedWithName": "{0} foi adicionado à biblioteca",
"ItemRemovedWithName": "{0} foi removido da biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca",

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save