Merge remote-tracking branch 'upstream/master' into library_scan_speed

pull/4242/head
Gary Wilber 4 years ago
commit e6d8c02944

@ -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

@ -0,0 +1,78 @@
parameters:
- name: LinuxImage
type: string
default: "ubuntu-latest"
- name: GeneratorVersion
type: string
default: "5.0.0-beta2"
jobs:
- job: GenerateApiClients
displayName: 'Generate Api Clients'
dependsOn: Test
pool:
vmImage: "${{ parameters.LinuxImage }}"
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec Artifact'
inputs:
source: 'current'
artifact: "OpenAPI Spec"
path: "$(System.ArtifactsDirectory)/openapispec"
runVersion: "latest"
- task: CmdLine@2
displayName: 'Download OpenApi Generator'
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"
## Authenticate with npm registry
- task: npmAuthenticate@0
inputs:
workingFile: ./.npmrc
customEndpoint: 'jellyfin-bot for NPM'
## Generate npm api client
# Unstable
- task: CmdLine@2
displayName: 'Build unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
# Stable
- task: CmdLine@2
displayName: 'Build stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
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
# Unstable
- task: Npm@1
displayName: 'Publish unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: publish
publishRegistry: useFeed
publishFeed: 'jellyfin/unstable'
workingDir: ./apiclient/generated/typescript/axios
# Stable
- task: Npm@1
displayName: 'Publish stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: publish
publishRegistry: useExternalRegistry
publishEndpoint: 'jellyfin-bot for NPM'
workingDir: ./apiclient/generated/typescript/axios

@ -65,6 +65,38 @@ jobs:
contents: '**' contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)' targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: OpenAPISpec
dependsOn: Test
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
displayName: 'Push OpenAPI Spec to repository'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec'
inputs:
source: 'current'
artifact: "OpenAPI Spec"
path: "$(System.ArtifactsDirectory)/openapispec"
runVersion: "latest"
- task: SSH@0
displayName: 'Create target directory on repository server'
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
- task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server'
inputs:
sshEndpoint: repository
sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
contents: 'openapi.json'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
- job: BuildDocker - job: BuildDocker
displayName: 'Build Docker' displayName: 'Build Docker'
@ -135,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'
@ -144,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'

@ -56,7 +56,7 @@ jobs:
inputs: inputs:
command: "test" command: "test"
projects: ${{ parameters.TestProjects }} projects: ${{ parameters.TestProjects }}
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"' arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
publishTestResults: true publishTestResults: true
testRunTitle: $(Agent.JobName) testRunTitle: $(Agent.JobName)
workingDirectory: "$(Build.SourcesDirectory)" workingDirectory: "$(Build.SourcesDirectory)"

@ -34,6 +34,12 @@ jobs:
Linux: 'ubuntu-latest' Linux: 'ubuntu-latest'
Windows: 'windows-latest' Windows: 'windows-latest'
macOS: 'macos-latest' macOS: 'macos-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-test.yml
parameters:
ImageNames:
Linux: 'ubuntu-latest'
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-abi.yml - template: azure-pipelines-abi.yml
@ -55,3 +61,6 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml - template: azure-pipelines-package.yml
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-api-client.yml

1
.gitignore vendored

@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts
web/ web/
web-src.* web-src.*
MediaBrowser.WebDashboard/jellyfin-web MediaBrowser.WebDashboard/jellyfin-web
apiclient/generated

@ -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

@ -137,6 +137,7 @@
- [KristupasSavickas](https://github.com/KristupasSavickas) - [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta) - [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen) - [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
# Emby Contributors # Emby Contributors

@ -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>();
@ -811,7 +811,7 @@ namespace Emby.Dlna.PlayTo
} }
/// <inheritdoc /> /// <inheritdoc />
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken) public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
{ {
if (_disposed) if (_disposed)
{ {
@ -823,17 +823,17 @@ namespace Emby.Dlna.PlayTo
return Task.CompletedTask; return Task.CompletedTask;
} }
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase)) if (name == SessionMessageType.Play)
{ {
return SendPlayCommand(data as PlayRequest, cancellationToken); return SendPlayCommand(data as PlayRequest, cancellationToken);
} }
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) if (name == SessionMessageType.PlayState)
{ {
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
} }
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) if (name == SessionMessageType.GeneralCommand)
{ {
return SendGeneralCommand(data as GeneralCommand, cancellationToken); return SendGeneralCommand(data as GeneralCommand, cancellationToken);
} }

@ -217,15 +217,15 @@ namespace Emby.Dlna.PlayTo
SupportedCommands = new[] SupportedCommands = new[]
{ {
GeneralCommandType.VolumeDown.ToString(), GeneralCommandType.VolumeDown,
GeneralCommandType.VolumeUp.ToString(), GeneralCommandType.VolumeUp,
GeneralCommandType.Mute.ToString(), GeneralCommandType.Mute,
GeneralCommandType.Unmute.ToString(), GeneralCommandType.Unmute,
GeneralCommandType.ToggleMute.ToString(), GeneralCommandType.ToggleMute,
GeneralCommandType.SetVolume.ToString(), GeneralCommandType.SetVolume,
GeneralCommandType.SetAudioStreamIndex.ToString(), GeneralCommandType.SetAudioStreamIndex,
GeneralCommandType.SetSubtitleStreamIndex.ToString(), GeneralCommandType.SetSubtitleStreamIndex,
GeneralCommandType.PlayMediaSource.ToString() GeneralCommandType.PlayMediaSource
}, },
SupportsMediaControl = true SupportsMediaControl = true

@ -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

@ -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;
@ -97,6 +95,7 @@ using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles; using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -127,7 +126,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 +256,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 +337,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 +401,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>();
@ -537,6 +535,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TvdbClientManager>(); ServiceCollection.AddSingleton<TvdbClientManager>();
ServiceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(_networkManager); ServiceCollection.AddSingleton(_networkManager);
@ -665,7 +664,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 +784,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 +816,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 +991,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 +1048,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 +1063,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 +1076,7 @@ namespace Emby.Server.Implementations
} }
} }
return dllList; return versions;
} }
/// <summary> /// <summary>
@ -1147,21 +1087,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);
@ -2921,7 +2925,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 +3329,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 +3914,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 +4755,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 +4832,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 +4905,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 +4957,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 +5002,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);
@ -5079,19 +5094,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 +5108,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 +5122,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 +5134,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 +5365,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 +5420,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 +5753,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 +5908,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 +6243,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" />

@ -17,6 +17,7 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints namespace Emby.Server.Implementations.EntryPoints
@ -106,7 +107,7 @@ namespace Emby.Server.Implementations.EntryPoints
try try
{ {
_sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None); _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
} }
catch catch
{ {
@ -124,7 +125,7 @@ namespace Emby.Server.Implementations.EntryPoints
try try
{ {
_sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None); _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
} }
catch catch
{ {
@ -348,7 +349,7 @@ namespace Emby.Server.Implementations.EntryPoints
try try
{ {
await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false); await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {

@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints namespace Emby.Server.Implementations.EntryPoints
@ -46,25 +47,25 @@ namespace Emby.Server.Implementations.EntryPoints
private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e) private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{ {
await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false); await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
} }
private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e) private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{ {
await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false); await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
} }
private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e) private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{ {
await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false); await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
} }
private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e) private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{ {
await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false); await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
} }
private async Task SendMessage(string name, TimerEventInfo info) private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
{ {
var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList(); var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();

@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken) private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
{ {
return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken); return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
} }
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems) private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)

@ -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;
} }
} }

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Json; using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.HttpServer
Connection = this Connection = this
}; };
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal)) if (info.MessageType == SessionMessageType.KeepAlive)
{ {
await SendKeepAliveResponse().ConfigureAwait(false); await SendKeepAliveResponse().ConfigureAwait(false);
} }
@ -244,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer
new WebSocketMessage<string> new WebSocketMessage<string>
{ {
MessageId = Guid.NewGuid(), MessageId = Guid.NewGuid(),
MessageType = "KeepAlive" MessageType = SessionMessageType.KeepAlive
}, CancellationToken.None); }, CancellationToken.None);
} }

@ -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,

@ -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",

@ -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",

@ -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",

@ -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",

@ -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",

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

Loading…
Cancel
Save