Merge remote-tracking branch 'upstream/master' into library_scan_speed
This commit is contained in:
commit
e6d8c02944
|
@ -62,6 +62,7 @@ jobs:
|
|||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
inputs:
|
||||
source: "specific"
|
||||
artifact: "$(NugetPackageName)"
|
||||
|
@ -73,6 +74,7 @@ jobs:
|
|||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
|
||||
contents: '**/*.dll'
|
||||
|
@ -83,6 +85,7 @@ jobs:
|
|||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Execute ABI Compatibility Check Tool'
|
||||
enabled: false
|
||||
inputs:
|
||||
command: custom
|
||||
custom: compat
|
||||
|
|
78
.ci/azure-pipelines-api-client.yml
Normal file
78
.ci/azure-pipelines-api-client.yml
Normal file
|
@ -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: '**'
|
||||
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
|
||||
displayName: 'Build Docker'
|
||||
|
||||
|
@ -135,7 +167,7 @@ jobs:
|
|||
inputs:
|
||||
sshEndpoint: repository
|
||||
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
|
||||
displayName: 'Update Stable Repository'
|
||||
|
@ -144,7 +176,7 @@ jobs:
|
|||
inputs:
|
||||
sshEndpoint: repository
|
||||
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
|
||||
displayName: 'Publish NuGet packages'
|
||||
|
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
inputs:
|
||||
command: "test"
|
||||
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
|
||||
testRunTitle: $(Agent.JobName)
|
||||
workingDirectory: "$(Build.SourcesDirectory)"
|
||||
|
|
|
@ -34,6 +34,12 @@ jobs:
|
|||
Linux: 'ubuntu-latest'
|
||||
Windows: 'windows-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'))) }}:
|
||||
- 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')) }}:
|
||||
- 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
1
.gitignore
vendored
|
@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts
|
|||
web/
|
||||
web-src.*
|
||||
MediaBrowser.WebDashboard/jellyfin-web
|
||||
apiclient/generated
|
||||
|
|
3
.npmrc
Normal file
3
.npmrc
Normal file
|
@ -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)
|
||||
- [Pusta](https://github.com/pusta)
|
||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||
- [skyfrk](https://github.com/skyfrk)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
|
|
@ -487,7 +487,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
User = user,
|
||||
Recursive = true,
|
||||
IsMissing = false,
|
||||
ExcludeItemTypes = new[] { typeof(Book).Name },
|
||||
ExcludeItemTypes = new[] { nameof(Book) },
|
||||
IsFolder = isFolder,
|
||||
MediaTypes = mediaTypes,
|
||||
DtoOptions = GetDtoOptions()
|
||||
|
@ -556,7 +556,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
IsVirtualItem = false,
|
||||
ExcludeItemTypes = new[] { typeof(Book).Name },
|
||||
ExcludeItemTypes = new[] { nameof(Book) },
|
||||
IsPlaceHolder = false,
|
||||
DtoOptions = GetDtoOptions()
|
||||
};
|
||||
|
@ -575,7 +575,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
};
|
||||
query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
|
||||
|
||||
SetSorting(query, sort, false);
|
||||
|
||||
|
@ -910,7 +910,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
|
||||
query.IncludeItemTypes = new[] { typeof(Series).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(Series) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -923,7 +923,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
|
||||
query.IncludeItemTypes = new[] { typeof(Movie).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(Movie) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -936,7 +936,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
// query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
|
||||
query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(BoxSet) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
|
||||
query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -962,7 +962,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
|
||||
query.IncludeItemTypes = new[] { typeof(Audio).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(Audio) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -975,7 +975,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
query.IsFavorite = true;
|
||||
query.IncludeItemTypes = new[] { typeof(Audio).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(Audio) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -988,7 +988,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
query.IsFavorite = true;
|
||||
query.IncludeItemTypes = new[] { typeof(Series).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(Series) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -1001,7 +1001,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
query.IsFavorite = true;
|
||||
query.IncludeItemTypes = new[] { typeof(Episode).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(Episode) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -1014,7 +1014,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
query.IsFavorite = true;
|
||||
query.IncludeItemTypes = new[] { typeof(Movie).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(Movie) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -1027,7 +1027,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
query.Parent = parent;
|
||||
query.SetUser(user);
|
||||
query.IsFavorite = true;
|
||||
query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name };
|
||||
query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
|
||||
|
||||
var result = _libraryManager.GetItemsResult(query);
|
||||
|
||||
|
@ -1181,7 +1181,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
{
|
||||
UserId = user.Id,
|
||||
Limit = 50,
|
||||
IncludeItemTypes = new[] { typeof(Episode).Name },
|
||||
IncludeItemTypes = new[] { nameof(Episode) },
|
||||
ParentId = parent == null ? Guid.Empty : parent.Id,
|
||||
GroupItems = false
|
||||
},
|
||||
|
@ -1215,7 +1215,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
Recursive = true,
|
||||
ParentId = parentId,
|
||||
ArtistIds = new[] { item.Id },
|
||||
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
|
||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
DtoOptions = GetDtoOptions()
|
||||
|
@ -1259,7 +1259,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
Recursive = true,
|
||||
ParentId = parentId,
|
||||
GenreIds = new[] { item.Id },
|
||||
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
|
||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
DtoOptions = GetDtoOptions()
|
||||
|
@ -1346,8 +1346,8 @@ namespace Emby.Dlna.ContentDirectory
|
|||
{
|
||||
if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stubType = (StubType)Enum.Parse(typeof(StubType), name, true);
|
||||
id = id.Split(new[] { '_' }, 2)[1];
|
||||
stubType = Enum.Parse<StubType>(name, true);
|
||||
id = id.Split('_', 2)[1];
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
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)
|
||||
{
|
||||
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);
|
||||
|
|
|
@ -383,9 +383,9 @@ namespace Emby.Dlna
|
|||
continue;
|
||||
}
|
||||
|
||||
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
|
||||
|
||||
var path = Path.Combine(systemProfilesPath, filename);
|
||||
var path = Path.Join(
|
||||
systemProfilesPath,
|
||||
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
|
||||
|
||||
using (var stream = _assembly.GetManifestResourceStream(name))
|
||||
{
|
||||
|
|
|
@ -168,7 +168,7 @@ namespace Emby.Dlna.Eventing
|
|||
|
||||
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.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
|
||||
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
|
||||
|
|
|
@ -257,9 +257,10 @@ namespace Emby.Dlna.Main
|
|||
|
||||
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 descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
|
@ -279,7 +280,6 @@ namespace Emby.Dlna.Main
|
|||
|
||||
_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 device = new SsdpRootDevice
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
|
@ -10,8 +8,16 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ControlHandler" />.
|
||||
/// </summary>
|
||||
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)
|
||||
: base(config, logger)
|
||||
{
|
||||
|
@ -35,9 +41,17 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
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)
|
||||
=> 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)
|
||||
=> xmlWriter.WriteElementString("Result", "1");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Service;
|
||||
|
@ -8,10 +6,19 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="MediaReceiverRegistrarService" />.
|
||||
/// </summary>
|
||||
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
|
||||
{
|
||||
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(
|
||||
ILogger<MediaReceiverRegistrarService> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
|
@ -24,7 +31,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
/// <inheritdoc />
|
||||
public string GetServiceXml()
|
||||
{
|
||||
return new MediaReceiverRegistrarXmlBuilder().GetXml();
|
||||
return MediaReceiverRegistrarXmlBuilder.GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
@ -1,79 +1,89 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
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(
|
||||
new ServiceActionListBuilder().GetActions(),
|
||||
GetStateVariables());
|
||||
return new ServiceXmlBuilder().GetXml(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()
|
||||
{
|
||||
var list = new List<StateVariable>();
|
||||
|
||||
list.Add(new StateVariable
|
||||
var list = new List<StateVariable>
|
||||
{
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
new StateVariable
|
||||
{
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_DeviceID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_DeviceID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
new StateVariable
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
new StateVariable
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationRespMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
});
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationRespMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationReqMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
});
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationReqMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
new StateVariable
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "int",
|
||||
SendsEvents = false
|
||||
});
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "int",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
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[]
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
|
@ -43,6 +53,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "IsAuthorized".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetIsAuthorized()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
|
@ -65,6 +79,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "RegisterDevice".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetRegisterDevice()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
|
@ -87,6 +105,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetValidationSucceededUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetValidationSucceededUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
|
@ -103,7 +125,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
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
|
||||
{
|
||||
|
@ -119,7 +145,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
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
|
||||
{
|
||||
|
@ -135,7 +165,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
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
|
||||
{
|
||||
|
|
|
@ -326,7 +326,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
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);
|
||||
|
||||
|
@ -339,7 +339,7 @@ namespace Emby.Dlna.PlayTo
|
|||
var startIndex = command.StartIndex ?? 0;
|
||||
if (startIndex > 0)
|
||||
{
|
||||
items = items.Skip(startIndex).ToList();
|
||||
items = items.GetRange(startIndex, items.Count - startIndex);
|
||||
}
|
||||
|
||||
var playlist = new List<PlaylistItem>();
|
||||
|
@ -811,7 +811,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
|
@ -823,17 +823,17 @@ namespace Emby.Dlna.PlayTo
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
|
||||
if (name == SessionMessageType.Play)
|
||||
{
|
||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
|
||||
if (name == SessionMessageType.PlayState)
|
||||
{
|
||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
|
||||
if (name == SessionMessageType.GeneralCommand)
|
||||
{
|
||||
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
|
||||
}
|
||||
|
|
|
@ -217,15 +217,15 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
SupportedCommands = new[]
|
||||
{
|
||||
GeneralCommandType.VolumeDown.ToString(),
|
||||
GeneralCommandType.VolumeUp.ToString(),
|
||||
GeneralCommandType.Mute.ToString(),
|
||||
GeneralCommandType.Unmute.ToString(),
|
||||
GeneralCommandType.ToggleMute.ToString(),
|
||||
GeneralCommandType.SetVolume.ToString(),
|
||||
GeneralCommandType.SetAudioStreamIndex.ToString(),
|
||||
GeneralCommandType.SetSubtitleStreamIndex.ToString(),
|
||||
GeneralCommandType.PlayMediaSource.ToString()
|
||||
GeneralCommandType.VolumeDown,
|
||||
GeneralCommandType.VolumeUp,
|
||||
GeneralCommandType.Mute,
|
||||
GeneralCommandType.Unmute,
|
||||
GeneralCommandType.ToggleMute,
|
||||
GeneralCommandType.SetVolume,
|
||||
GeneralCommandType.SetAudioStreamIndex,
|
||||
GeneralCommandType.SetSubtitleStreamIndex,
|
||||
GeneralCommandType.PlayMediaSource
|
||||
},
|
||||
|
||||
SupportsMediaControl = true
|
||||
|
|
|
@ -235,13 +235,13 @@ namespace Emby.Dlna.Server
|
|||
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
|
||||
.Append("</serviceId>");
|
||||
builder.Append("<SCPDURL>")
|
||||
.Append(BuildUrl(service.ScpdUrl, true))
|
||||
.Append(BuildUrl(service.ScpdUrl))
|
||||
.Append("</SCPDURL>");
|
||||
builder.Append("<controlURL>")
|
||||
.Append(BuildUrl(service.ControlUrl, true))
|
||||
.Append(BuildUrl(service.ControlUrl))
|
||||
.Append("</controlURL>");
|
||||
builder.Append("<eventSubURL>")
|
||||
.Append(BuildUrl(service.EventSubUrl, true))
|
||||
.Append(BuildUrl(service.EventSubUrl))
|
||||
.Append("</eventSubURL>");
|
||||
|
||||
builder.Append("</service>");
|
||||
|
@ -250,13 +250,7 @@ namespace Emby.Dlna.Server
|
|||
builder.Append("</serviceList>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
private string BuildUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
|
@ -267,7 +261,7 @@ namespace Emby.Dlna.Server
|
|||
|
||||
url = "/dlna/" + _serverUdn + "/" + url;
|
||||
|
||||
if (EnableAbsoluteUrls || absoluteUrl)
|
||||
if (EnableAbsoluteUrls)
|
||||
{
|
||||
url = _serverAddress.TrimEnd('/') + url;
|
||||
}
|
||||
|
|
|
@ -60,10 +60,8 @@ namespace Emby.Dlna.Service
|
|||
Async = true
|
||||
};
|
||||
|
||||
using (var reader = XmlReader.Create(streamReader, readerSettings))
|
||||
{
|
||||
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
|
||||
}
|
||||
using var reader = XmlReader.Create(streamReader, readerSettings);
|
||||
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
|
||||
|
@ -124,10 +122,8 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
|
||||
}
|
||||
using var subReader = reader.ReadSubtree();
|
||||
return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
|
||||
}
|
||||
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)
|
||||
{
|
||||
var result = new ControlRequestInfo();
|
||||
string namespaceURI = null, localName = null;
|
||||
|
||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
|
@ -165,16 +161,15 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
result.LocalName = reader.LocalName;
|
||||
result.NamespaceURI = reader.NamespaceURI;
|
||||
localName = reader.LocalName;
|
||||
namespaceURI = reader.NamespaceURI;
|
||||
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
var result = new ControlRequestInfo(localName, namespaceURI);
|
||||
using var subReader = reader.ReadSubtree();
|
||||
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
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)
|
||||
|
@ -234,11 +234,18 @@ namespace Emby.Dlna.Service
|
|||
|
||||
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 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 IMediaEncoder _mediaEncoder;
|
||||
|
||||
private bool _disposed = false;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageProcessor"/> class.
|
||||
|
@ -466,11 +466,11 @@ namespace Emby.Drawing
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateImageCollage(ImageCollageOptions options)
|
||||
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
|
||||
{
|
||||
_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);
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ namespace Emby.Drawing
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateImageCollage(ImageCollageOptions options)
|
||||
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#nullable enable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
@ -16,21 +17,11 @@ namespace Emby.Naming.AudioBook
|
|||
_options = options;
|
||||
}
|
||||
|
||||
public AudioBookFileInfo ParseFile(string path)
|
||||
public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
|
||||
{
|
||||
return Resolve(path, false);
|
||||
}
|
||||
|
||||
public AudioBookFileInfo ParseDirectory(string path)
|
||||
{
|
||||
return Resolve(path, true);
|
||||
}
|
||||
|
||||
public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
if (path.Length == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
throw new ArgumentException("String can't be empty.", nameof(path));
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
|
|
@ -209,7 +209,10 @@ namespace Emby.Notifications
|
|||
_libraryUpdateTimer = null;
|
||||
}
|
||||
|
||||
items = items.Take(10).ToList();
|
||||
if (items.Count > 10)
|
||||
{
|
||||
items = items.GetRange(0, 10);
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
|
|
|
@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string VirtualDataPath { get; } = "%AppDataPath%";
|
||||
public string VirtualDataPath => "%AppDataPath%";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache path.
|
||||
|
|
|
@ -4,7 +4,6 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
|
|||
using Emby.Server.Implementations.Data;
|
||||
using Emby.Server.Implementations.Devices;
|
||||
using Emby.Server.Implementations.Dto;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Emby.Server.Implementations.HttpServer.Security;
|
||||
using Emby.Server.Implementations.IO;
|
||||
using Emby.Server.Implementations.Library;
|
||||
|
@ -97,6 +95,7 @@ using MediaBrowser.Model.Tasks;
|
|||
using MediaBrowser.Providers.Chapters;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
using MediaBrowser.Providers.Plugins.TheTvdb;
|
||||
using MediaBrowser.Providers.Plugins.Tmdb;
|
||||
using MediaBrowser.Providers.Subtitles;
|
||||
using MediaBrowser.XbmcMetadata.Providers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -127,7 +126,6 @@ namespace Emby.Server.Implementations
|
|||
private IMediaEncoder _mediaEncoder;
|
||||
private ISessionManager _sessionManager;
|
||||
private IHttpClientFactory _httpClientFactory;
|
||||
private IWebSocketManager _webSocketManager;
|
||||
|
||||
private string[] _urlPrefixes;
|
||||
|
||||
|
@ -258,8 +256,8 @@ namespace Emby.Server.Implementations
|
|||
IServiceCollection serviceCollection)
|
||||
{
|
||||
_xmlSerializer = new MyXmlSerializer();
|
||||
_jsonSerializer = new JsonSerializer();
|
||||
|
||||
_jsonSerializer = new JsonSerializer();
|
||||
|
||||
ServiceCollection = serviceCollection;
|
||||
|
||||
_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.
|
||||
/// Presently used to provide contact information to MusicBrainz service.
|
||||
/// </summary>
|
||||
public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org";
|
||||
public string ApplicationUserAgentAddress => "team@jellyfin.org";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current application name.
|
||||
|
@ -403,7 +401,7 @@ namespace Emby.Server.Implementations
|
|||
/// <summary>
|
||||
/// Resolves this instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type</typeparam>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
/// <returns>``0.</returns>
|
||||
public T Resolve<T>() => ServiceProvider.GetService<T>();
|
||||
|
||||
|
@ -537,6 +535,7 @@ namespace Emby.Server.Implementations
|
|||
|
||||
ServiceCollection.AddSingleton(_fileSystemManager);
|
||||
ServiceCollection.AddSingleton<TvdbClientManager>();
|
||||
ServiceCollection.AddSingleton<TmdbClientManager>();
|
||||
|
||||
ServiceCollection.AddSingleton(_networkManager);
|
||||
|
||||
|
@ -665,7 +664,6 @@ namespace Emby.Server.Implementations
|
|||
_mediaEncoder = Resolve<IMediaEncoder>();
|
||||
_sessionManager = Resolve<ISessionManager>();
|
||||
_httpClientFactory = Resolve<IHttpClientFactory>();
|
||||
_webSocketManager = Resolve<IWebSocketManager>();
|
||||
|
||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||
|
||||
|
@ -786,7 +784,6 @@ namespace Emby.Server.Implementations
|
|||
.ToArray();
|
||||
|
||||
_urlPrefixes = GetUrlPrefixes().ToArray();
|
||||
_webSocketManager.Init(GetExports<IWebSocketListener>());
|
||||
|
||||
Resolve<ILibraryManager>().AddParts(
|
||||
GetExports<IResolverIgnoreRule>(),
|
||||
|
@ -819,38 +816,6 @@ namespace Emby.Server.Implementations
|
|||
{
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -1026,80 +991,54 @@ namespace Emby.Server.Implementations
|
|||
|
||||
protected abstract void RestartInternal();
|
||||
|
||||
/// <summary>
|
||||
/// Comparison function used in <see cref="GetPlugins" />.
|
||||
/// </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)
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
|
||||
{
|
||||
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 versions = new List<(Version PluginVersion, string Name, string Path)>();
|
||||
var minimumVersion = new Version(0, 0, 0, 1);
|
||||
var versions = new List<LocalPlugin>();
|
||||
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
|
||||
string metafile;
|
||||
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
try
|
||||
{
|
||||
metafile = Path.Combine(dir, "meta.json");
|
||||
var metafile = Path.Combine(dir, "meta.json");
|
||||
if (File.Exists(metafile))
|
||||
{
|
||||
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
|
||||
|
||||
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))
|
||||
{
|
||||
version = new Version(0, 0, 0, 1);
|
||||
version = minimumVersion;
|
||||
}
|
||||
|
||||
if (ApplicationVersion >= targetAbi)
|
||||
{
|
||||
// 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
|
||||
{
|
||||
// No metafile, so lets see if the folder is versioned.
|
||||
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
|
||||
|
||||
|
||||
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.
|
||||
versions.Add((ver, metafile, dir));
|
||||
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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));
|
||||
}
|
||||
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
|
||||
versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -1109,14 +1048,14 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
|
||||
string lastName = string.Empty;
|
||||
versions.Sort(VersionCompare);
|
||||
versions.Sort(LocalPlugin.Compare);
|
||||
// Traverse backwards through the list.
|
||||
// The first item will be the latest version.
|
||||
for (int x = versions.Count - 1; x >= 0; x--)
|
||||
{
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
|
@ -1124,6 +1063,7 @@ namespace Emby.Server.Implementations
|
|||
if (!string.IsNullOrEmpty(lastName) && cleanup)
|
||||
{
|
||||
// Attempt a cleanup of old folders.
|
||||
versions.RemoveAt(x);
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Deleting {Path}", versions[x].Path);
|
||||
|
@ -1136,7 +1076,7 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
}
|
||||
|
||||
return dllList;
|
||||
return versions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1147,21 +1087,24 @@ namespace Emby.Server.Implementations
|
|||
{
|
||||
if (Directory.Exists(ApplicationPaths.PluginsPath))
|
||||
{
|
||||
foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
|
||||
foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
|
||||
{
|
||||
Assembly plugAss;
|
||||
try
|
||||
foreach (var file in plugin.DllFiles)
|
||||
{
|
||||
plugAss = Assembly.LoadFrom(file);
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load assembly {Path}", file);
|
||||
continue;
|
||||
}
|
||||
Assembly plugAss;
|
||||
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);
|
||||
yield return plugAss;
|
||||
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
|
||||
yield return plugAss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
|
|||
var all = channels;
|
||||
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)
|
||||
{
|
||||
foreach (var item in returnItems)
|
||||
foreach (var item in all)
|
||||
{
|
||||
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
return new QueryResult<Channel>
|
||||
{
|
||||
Items = returnItems,
|
||||
Items = all,
|
||||
TotalRecordCount = totalCount
|
||||
};
|
||||
}
|
||||
|
@ -543,7 +538,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
return _libraryManager.GetItemIds(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Channel).Name },
|
||||
IncludeItemTypes = new[] { nameof(Channel) },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
|
||||
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Channel).Name },
|
||||
IncludeItemTypes = new[] { nameof(Channel) },
|
||||
ExcludeItemIds = installedChannelIds.ToArray()
|
||||
});
|
||||
|
||||
|
|
|
@ -157,7 +157,8 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
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"))
|
||||
{
|
||||
|
|
|
@ -219,7 +219,8 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
connection.RunQueries(queries);
|
||||
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
var existingColumnNames = GetColumnNames(db, "AncestorIds");
|
||||
AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
|
||||
|
@ -495,7 +496,8 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
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())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
SaveItemsInTranscation(db, tuples);
|
||||
}, TransactionMode);
|
||||
|
@ -2032,7 +2035,8 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
// First delete chapters
|
||||
db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
|
||||
|
@ -2921,7 +2925,8 @@ namespace Emby.Server.Implementations.Data
|
|||
var result = new QueryResult<BaseItem>();
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
var statements = PrepareAll(db, statementTexts);
|
||||
|
||||
|
@ -3324,7 +3329,8 @@ namespace Emby.Server.Implementations.Data
|
|||
var result = new QueryResult<Guid>();
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
var statements = PrepareAll(db, statementTexts);
|
||||
|
||||
|
@ -3908,7 +3914,7 @@ namespace Emby.Server.Implementations.Data
|
|||
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
|
||||
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)
|
||||
{
|
||||
|
@ -4749,29 +4755,29 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
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;
|
||||
|
@ -4826,12 +4832,12 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
var types = new[]
|
||||
{
|
||||
typeof(Episode).Name,
|
||||
typeof(Video).Name,
|
||||
typeof(Movie).Name,
|
||||
typeof(MusicVideo).Name,
|
||||
typeof(Series).Name,
|
||||
typeof(Season).Name
|
||||
nameof(Episode),
|
||||
nameof(Video),
|
||||
nameof(Movie),
|
||||
nameof(MusicVideo),
|
||||
nameof(Series),
|
||||
nameof(Season)
|
||||
};
|
||||
|
||||
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())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
connection.ExecuteAll(sql);
|
||||
}, TransactionMode);
|
||||
|
@ -4950,7 +4957,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
var idBlob = id.ToByteArray();
|
||||
|
||||
|
@ -4994,26 +5002,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
commandText += " LIMIT " + query.Limit;
|
||||
commandText.Append(" LIMIT ").Append(query.Limit);
|
||||
}
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
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
|
||||
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))
|
||||
{
|
||||
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))
|
||||
{
|
||||
whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
|
||||
}
|
||||
whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
|
||||
statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
whereClauses.Add("PersonType=@PersonType");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@PersonType", queryPersonTypes[0]);
|
||||
}
|
||||
statement?.TryBind("@PersonType", queryPersonTypes[0]);
|
||||
}
|
||||
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)
|
||||
{
|
||||
whereClauses.Add("PersonType<>@PersonType");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
|
||||
}
|
||||
statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
|
||||
}
|
||||
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)
|
||||
{
|
||||
whereClauses.Add("ListOrder<=@MaxListOrder");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
|
||||
}
|
||||
statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.NameContains))
|
||||
{
|
||||
whereClauses.Add("Name like @NameContains");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@NameContains", "%" + query.NameContains + "%");
|
||||
}
|
||||
whereClauses.Add("p.Name like @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;
|
||||
|
@ -5357,7 +5365,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
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,
|
||||
Tags = query.Tags,
|
||||
OfficialRatings = query.OfficialRatings,
|
||||
StudioIds = query.StudioIds,
|
||||
GenreIds = query.GenreIds,
|
||||
Genres = query.Genres,
|
||||
Years = query.Years,
|
||||
|
@ -5744,7 +5753,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
var itemIdBlob = itemId.ToByteArray();
|
||||
|
||||
|
@ -5898,7 +5908,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
var itemIdBlob = id.ToByteArray();
|
||||
|
||||
|
@ -6232,7 +6243,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
var itemIdBlob = id.ToByteArray();
|
||||
|
||||
|
|
|
@ -44,7 +44,8 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
var users = userDatasTableExists ? null : userManager.Users;
|
||||
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
db.ExecuteAll(string.Join(";", new[] {
|
||||
|
||||
|
@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
SaveUserData(db, internalUserId, key, userData);
|
||||
}, TransactionMode);
|
||||
|
@ -246,7 +248,8 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
foreach (var userItemData in userDataList)
|
||||
{
|
||||
|
|
|
@ -465,7 +465,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
{
|
||||
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
|
||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
||||
Name = item.Album,
|
||||
Limit = 1
|
||||
});
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" 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.Extensions.DependencyInjection" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.0" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
|
||||
|
|
|
@ -17,6 +17,7 @@ using MediaBrowser.Controller.Plugins;
|
|||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.EntryPoints
|
||||
|
@ -106,7 +107,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
try
|
||||
{
|
||||
_sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None);
|
||||
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -124,7 +125,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
try
|
||||
{
|
||||
_sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None);
|
||||
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -348,7 +349,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.EntryPoints
|
||||
|
@ -46,25 +47,25 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
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)
|
||||
{
|
||||
await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
|
||||
await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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();
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
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)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
public AuthorizationInfo Authenticate(HttpRequest 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.");
|
||||
}
|
||||
|
|
|
@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(requestContext);
|
||||
var (authInfo, _) =
|
||||
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
|
@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(httpReq);
|
||||
var (authInfo, originalAuthInfo) =
|
||||
GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
||||
|
||||
if (originalAuthInfo != null)
|
||||
{
|
||||
httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
|
||||
}
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
||||
|
||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
|
||||
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
||||
in Dictionary<string, string> auth,
|
||||
in IHeaderDictionary headers,
|
||||
in IQueryCollection queryString)
|
||||
|
@ -108,88 +101,102 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
Device = device,
|
||||
DeviceId = deviceId,
|
||||
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;
|
||||
}
|
||||
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
AccessToken = token
|
||||
});
|
||||
|
||||
if (result.Items.Count > 0)
|
||||
{
|
||||
authInfo.IsAuthenticated = true;
|
||||
}
|
||||
|
||||
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||
|
||||
if (originalAuthenticationInfo != null)
|
||||
{
|
||||
var updateToken = false;
|
||||
|
||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||
{
|
||||
AccessToken = token
|
||||
});
|
||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||
}
|
||||
|
||||
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||
|
||||
if (originalAuthenticationInfo != null)
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
{
|
||||
var updateToken = false;
|
||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||
}
|
||||
|
||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||
{
|
||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||
}
|
||||
// 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;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||
}
|
||||
}
|
||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||
{
|
||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||
}
|
||||
}
|
||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||
|
||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||
authInfo.IsApiKey = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
authInfo.IsApiKey = false;
|
||||
}
|
||||
|
||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||
updateToken = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateToken)
|
||||
{
|
||||
_authRepo.Update(originalAuthenticationInfo);
|
||||
}
|
||||
if (updateToken)
|
||||
{
|
||||
_authRepo.Update(originalAuthenticationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return (authInfo, originalAuthenticationInfo);
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -267,7 +274,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
if (param.Length == 2)
|
||||
{
|
||||
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.Controller.Net;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
Connection = this
|
||||
};
|
||||
|
||||
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
|
||||
if (info.MessageType == SessionMessageType.KeepAlive)
|
||||
{
|
||||
await SendKeepAliveResponse().ConfigureAwait(false);
|
||||
}
|
||||
|
@ -244,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
new WebSocketMessage<string>
|
||||
{
|
||||
MessageId = Guid.NewGuid(),
|
||||
MessageType = "KeepAlive"
|
||||
MessageType = SessionMessageType.KeepAlive
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
|
@ -14,16 +13,18 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
{
|
||||
public class WebSocketManager : IWebSocketManager
|
||||
{
|
||||
private readonly Lazy<IEnumerable<IWebSocketListener>> _webSocketListeners;
|
||||
private readonly ILogger<WebSocketManager> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
|
||||
private bool _disposed = false;
|
||||
|
||||
public WebSocketManager(
|
||||
Lazy<IEnumerable<IWebSocketListener>> webSocketListeners,
|
||||
ILogger<WebSocketManager> logger,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_webSocketListeners = webSocketListeners;
|
||||
_logger = logger;
|
||||
_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>
|
||||
/// Processes the web socket message received.
|
||||
/// </summary>
|
||||
|
@ -90,7 +82,8 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
foreach (var x in _webSocketListeners)
|
||||
var listeners = _webSocketListeners.Value;
|
||||
foreach (var x in listeners)
|
||||
{
|
||||
yield return x.ProcessMessageAsync(result);
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Images
|
|||
// return _libraryManager.GetItemList(new InternalItemsQuery
|
||||
// {
|
||||
// ArtistIds = new[] { item.Id },
|
||||
// IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
|
||||
// IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
||||
// OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
||||
// Limit = 4,
|
||||
// Recursive = true,
|
||||
|
|
|
@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images
|
|||
|
||||
protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items)
|
||||
{
|
||||
var useBackdrop = primaryItem is CollectionFolder;
|
||||
return items
|
||||
.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);
|
||||
if (image != null && image.IsLocalFile)
|
||||
{
|
||||
|
@ -190,7 +201,7 @@ namespace Emby.Server.Implementations.Images
|
|||
return null;
|
||||
}
|
||||
|
||||
ImageProcessor.CreateImageCollage(options);
|
||||
ImageProcessor.CreateImageCollage(options, primaryItem.Name);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,12 @@ namespace Emby.Server.Implementations.Images
|
|||
return _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
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) },
|
||||
Limit = 4,
|
||||
Recursive = true,
|
||||
|
@ -77,7 +82,7 @@ namespace Emby.Server.Implementations.Images
|
|||
return _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Genres = new[] { item.Name },
|
||||
IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name },
|
||||
IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
|
||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
||||
Limit = 4,
|
||||
Recursive = true,
|
||||
|
|
|
@ -2440,6 +2440,21 @@ namespace Emby.Server.Implementations.Library
|
|||
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 />
|
||||
public bool IsVideoFile(string path)
|
||||
{
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly ILocalizationManager _localizationManager;
|
||||
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 IMediaSourceProvider[] _providers;
|
||||
|
@ -582,29 +583,20 @@ namespace Emby.Server.Implementations.Library
|
|||
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);
|
||||
|
||||
try
|
||||
var info = _openStreams.Values.FirstOrDefault(i =>
|
||||
{
|
||||
var info = _openStreams.Values.FirstOrDefault(i =>
|
||||
var liveStream = i as ILiveStream;
|
||||
if (liveStream != null)
|
||||
{
|
||||
var liveStream = i as ILiveStream;
|
||||
if (liveStream != null)
|
||||
{
|
||||
return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
return info as IDirectStreamProvider;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_liveStreamSemaphore.Release();
|
||||
}
|
||||
return Task.FromResult(info as IDirectStreamProvider);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
|
||||
private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
if (_openStreams.TryGetValue(id, out ILiveStream info))
|
||||
{
|
||||
if (_openStreams.TryGetValue(id, out ILiveStream info))
|
||||
{
|
||||
return info;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
return Task.FromResult(info);
|
||||
}
|
||||
finally
|
||||
else
|
||||
{
|
||||
_liveStreamSemaphore.Release();
|
||||
return Task.FromException<ILiveStream>(new ResourceNotFoundException());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -844,7 +827,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (liveStream.ConsumerCount <= 0)
|
||||
{
|
||||
_openStreams.Remove(id);
|
||||
_openStreams.TryRemove(id, out _);
|
||||
|
||||
_logger.LogInformation("Closing live stream {0}", id);
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Library
|
|||
var genres = item
|
||||
.GetRecursiveChildren(user, new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Audio).Name },
|
||||
IncludeItemTypes = new[] { nameof(Audio) },
|
||||
DtoOptions = dtoOptions
|
||||
})
|
||||
.Cast<Audio>()
|
||||
|
@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
return _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Audio).Name },
|
||||
IncludeItemTypes = new[] { nameof(Audio) },
|
||||
|
||||
GenreIds = genreIds.ToArray(),
|
||||
|
||||
|
|
|
@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Fourth;
|
||||
|
||||
public MultiItemResolverResult ResolveMultiple(Folder parent,
|
||||
public MultiItemResolverResult ResolveMultiple(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
|
@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
return result;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
|
||||
private MultiItemResolverResult ResolveMultipleInternal(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Audio;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -113,52 +116,48 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
IFileSystem fileSystem,
|
||||
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 notMultiDisc = false;
|
||||
|
||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
logger.LogDebug("Found multi-disc folder: " + path);
|
||||
Interlocked.Increment(ref discSubfolderCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
|
||||
state.Stop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var fullName = fileSystemInfo.FullName;
|
||||
});
|
||||
|
||||
if (libraryManager.IsAudioFile(fullName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notMultiDisc)
|
||||
if (!result.IsCompleted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
|
||||
|
||||
// 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) ??
|
||||
string.Empty;
|
||||
|
||||
return _validExtensions.Contains(fileExtension,
|
||||
return _validExtensions.Contains(
|
||||
fileExtension,
|
||||
StringComparer
|
||||
.OrdinalIgnoreCase);
|
||||
}).ToList();
|
||||
|
|
|
@ -87,61 +87,61 @@ namespace Emby.Server.Implementations.Library
|
|||
var excludeItemTypes = query.ExcludeItemTypes.ToList();
|
||||
var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
|
||||
|
||||
excludeItemTypes.Add(typeof(Year).Name);
|
||||
excludeItemTypes.Add(typeof(Folder).Name);
|
||||
excludeItemTypes.Add(nameof(Year));
|
||||
excludeItemTypes.Add(nameof(Folder));
|
||||
|
||||
if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (!query.IncludeMedia)
|
||||
{
|
||||
AddIfMissing(includeItemTypes, typeof(Genre).Name);
|
||||
AddIfMissing(includeItemTypes, typeof(MusicGenre).Name);
|
||||
AddIfMissing(includeItemTypes, nameof(Genre));
|
||||
AddIfMissing(includeItemTypes, nameof(MusicGenre));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddIfMissing(excludeItemTypes, typeof(Genre).Name);
|
||||
AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name);
|
||||
AddIfMissing(excludeItemTypes, nameof(Genre));
|
||||
AddIfMissing(excludeItemTypes, nameof(MusicGenre));
|
||||
}
|
||||
|
||||
if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (!query.IncludeMedia)
|
||||
{
|
||||
AddIfMissing(includeItemTypes, typeof(Person).Name);
|
||||
AddIfMissing(includeItemTypes, nameof(Person));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddIfMissing(excludeItemTypes, typeof(Person).Name);
|
||||
AddIfMissing(excludeItemTypes, nameof(Person));
|
||||
}
|
||||
|
||||
if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (!query.IncludeMedia)
|
||||
{
|
||||
AddIfMissing(includeItemTypes, typeof(Studio).Name);
|
||||
AddIfMissing(includeItemTypes, nameof(Studio));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddIfMissing(excludeItemTypes, typeof(Studio).Name);
|
||||
AddIfMissing(excludeItemTypes, nameof(Studio));
|
||||
}
|
||||
|
||||
if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (!query.IncludeMedia)
|
||||
{
|
||||
AddIfMissing(includeItemTypes, typeof(MusicArtist).Name);
|
||||
AddIfMissing(includeItemTypes, nameof(MusicArtist));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name);
|
||||
AddIfMissing(excludeItemTypes, nameof(MusicArtist));
|
||||
}
|
||||
|
||||
AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name);
|
||||
AddIfMissing(excludeItemTypes, typeof(Folder).Name);
|
||||
AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
|
||||
AddIfMissing(excludeItemTypes, nameof(Folder));
|
||||
var mediaTypes = query.MediaTypes.ToList();
|
||||
|
||||
if (includeItemTypes.Count > 0)
|
||||
|
|
|
@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
|
|||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(MusicArtist).Name },
|
||||
IncludeItemTypes = new[] { nameof(MusicArtist) },
|
||||
IsDeadArtist = true,
|
||||
IsLocked = false
|
||||
}).Cast<MusicArtist>().ToList();
|
||||
|
|
|
@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Validators
|
|||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Person).Name },
|
||||
IncludeItemTypes = new[] { nameof(Person) },
|
||||
IsDeadPerson = true,
|
||||
IsLocked = false
|
||||
});
|
||||
|
|
|
@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators
|
|||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Studio).Name },
|
||||
IncludeItemTypes = new[] { nameof(Studio) },
|
||||
IsDeadStudio = true,
|
||||
IsLocked = false
|
||||
});
|
||||
|
|
|
@ -1790,7 +1790,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
{
|
||||
var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
|
||||
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
|
||||
Limit = 1,
|
||||
ExternalId = timer.ProgramId,
|
||||
DtoOptions = new DtoOptions(true)
|
||||
|
@ -2151,7 +2151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
{
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
Limit = 1,
|
||||
DtoOptions = new DtoOptions(true)
|
||||
{
|
||||
|
@ -2370,7 +2370,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
ExternalSeriesId = seriesTimer.SeriesId,
|
||||
DtoOptions = new DtoOptions(true)
|
||||
{
|
||||
|
@ -2405,7 +2405,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
channel = _libraryManager.GetItemList(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
|
||||
ItemIds = new[] { parent.ChannelId },
|
||||
DtoOptions = new DtoOptions()
|
||||
}).FirstOrDefault() as LiveTvChannel;
|
||||
|
@ -2464,7 +2464,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
channel = _libraryManager.GetItemList(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name },
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
|
||||
ItemIds = new[] { programInfo.ChannelId },
|
||||
DtoOptions = new DtoOptions()
|
||||
}).FirstOrDefault() as LiveTvChannel;
|
||||
|
@ -2529,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
var seriesIds = _libraryManager.GetItemIds(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Series).Name },
|
||||
IncludeItemTypes = new[] { nameof(Series) },
|
||||
Name = program.Name
|
||||
}).ToArray();
|
||||
|
||||
|
@ -2542,7 +2542,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
{
|
||||
var result = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(Episode).Name },
|
||||
IncludeItemTypes = new[] { nameof(Episode) },
|
||||
ParentIndexNumber = program.SeasonNumber.Value,
|
||||
IndexNumber = program.EpisodeNumber.Value,
|
||||
AncestorIds = seriesIds,
|
||||
|
|
|
@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(Series).Name },
|
||||
IncludeItemTypes = new string[] { nameof(Series) },
|
||||
Name = seriesName,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Thumb },
|
||||
|
@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(Series).Name },
|
||||
IncludeItemTypes = new string[] { nameof(Series) },
|
||||
Name = seriesName,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Thumb },
|
||||
|
@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
var program = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(Series).Name },
|
||||
IncludeItemTypes = new string[] { nameof(Series) },
|
||||
Name = seriesName,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
|
@ -307,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
program = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
ExternalSeriesId = programSeriesId,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
|
|
|
@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
IsKids = query.IsKids,
|
||||
IsSports = query.IsSports,
|
||||
IsSeries = query.IsSeries,
|
||||
IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
|
||||
IncludeItemTypes = new[] { nameof(LiveTvChannel) },
|
||||
TopParentIds = new[] { topFolder.Id },
|
||||
IsFavorite = query.IsFavorite,
|
||||
IsLiked = query.IsLiked,
|
||||
|
@ -808,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
var internalQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
|
||||
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
|
||||
MinEndDate = query.MinEndDate,
|
||||
MinStartDate = query.MinStartDate,
|
||||
MaxEndDate = query.MaxEndDate,
|
||||
|
@ -872,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
var internalQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
|
||||
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
|
||||
IsAiring = query.IsAiring,
|
||||
HasAired = query.HasAired,
|
||||
IsNews = query.IsNews,
|
||||
|
@ -1089,8 +1089,8 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
if (cleanDatabase)
|
||||
{
|
||||
CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken);
|
||||
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken);
|
||||
CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
|
||||
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
|
||||
}
|
||||
|
||||
var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
|
||||
|
@ -1181,7 +1181,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||
IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
|
||||
ChannelIds = new Guid[] { currentChannel.Id },
|
||||
DtoOptions = new DtoOptions(true)
|
||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||
|
@ -1346,11 +1346,11 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
if (query.IsMovie.Value)
|
||||
{
|
||||
includeItemTypes.Add(typeof(Movie).Name);
|
||||
includeItemTypes.Add(nameof(Movie));
|
||||
}
|
||||
else
|
||||
{
|
||||
excludeItemTypes.Add(typeof(Movie).Name);
|
||||
excludeItemTypes.Add(nameof(Movie));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1358,11 +1358,11 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
if (query.IsSeries.Value)
|
||||
{
|
||||
includeItemTypes.Add(typeof(Episode).Name);
|
||||
includeItemTypes.Add(nameof(Episode));
|
||||
}
|
||||
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)
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
|
||||
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
|
||||
ChannelIds = channelIds,
|
||||
MaxStartDate = now,
|
||||
MinEndDate = now,
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
return new[]
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
public string GetFilePath()
|
||||
{
|
||||
return TempFilePath;
|
||||
}
|
||||
|
||||
private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
|
|
|
@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
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)
|
||||
|
@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
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.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
_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.
|
||||
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)
|
||||
.GetStreamAsync(url);
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.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:";
|
||||
|
|
|
@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
var typeName = GetType().Name;
|
||||
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)
|
||||
.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)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
"ItemAddedWithName": "{0} is in die versameling",
|
||||
"HomeVideos": "Tuis opnames",
|
||||
"HeaderRecordingGroups": "Groep Opnames",
|
||||
"HeaderCameraUploads": "Kamera Oplaai",
|
||||
"Genres": "Genres",
|
||||
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
|
||||
"ChapterNameValue": "Hoofstuk",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "المجلدات",
|
||||
"Genres": "التضنيفات",
|
||||
"HeaderAlbumArtists": "فناني الألبومات",
|
||||
"HeaderCameraUploads": "تحميلات الكاميرا",
|
||||
"HeaderContinueWatching": "استئناف",
|
||||
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
||||
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Папки",
|
||||
"Genres": "Жанрове",
|
||||
"HeaderAlbumArtists": "Изпълнители на албуми",
|
||||
"HeaderCameraUploads": "Качени от камера",
|
||||
"HeaderContinueWatching": "Продължаване на гледането",
|
||||
"HeaderFavoriteAlbums": "Любими албуми",
|
||||
"HeaderFavoriteArtists": "Любими изпълнители",
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
|
||||
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
|
||||
"HeaderContinueWatching": "দেখতে থাকুন",
|
||||
"HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
|
||||
"HeaderAlbumArtists": "এলবাম শিল্পী",
|
||||
"Genres": "জেনার",
|
||||
"Folders": "ফোল্ডারগুলো",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Carpetes",
|
||||
"Genres": "Gèneres",
|
||||
"HeaderAlbumArtists": "Artistes del Àlbum",
|
||||
"HeaderCameraUploads": "Pujades de Càmera",
|
||||
"HeaderContinueWatching": "Continua Veient",
|
||||
"HeaderFavoriteAlbums": "Àlbums Preferits",
|
||||
"HeaderFavoriteArtists": "Artistes Preferits",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Složky",
|
||||
"Genres": "Žánry",
|
||||
"HeaderAlbumArtists": "Umělci alba",
|
||||
"HeaderCameraUploads": "Nahrané fotografie",
|
||||
"HeaderContinueWatching": "Pokračovat ve sledování",
|
||||
"HeaderFavoriteAlbums": "Oblíbená alba",
|
||||
"HeaderFavoriteArtists": "Oblíbení interpreti",
|
||||
|
@ -114,5 +113,7 @@
|
|||
"TasksChannelsCategory": "Internetové kanály",
|
||||
"TasksApplicationCategory": "Aplikace",
|
||||
"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",
|
||||
"Genres": "Genrer",
|
||||
"HeaderAlbumArtists": "Albumkunstnere",
|
||||
"HeaderCameraUploads": "Kamera Uploads",
|
||||
"HeaderContinueWatching": "Fortsæt Afspilning",
|
||||
"HeaderFavoriteAlbums": "Favoritalbummer",
|
||||
"HeaderFavoriteArtists": "Favoritkunstnere",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Verzeichnisse",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Album-Interpreten",
|
||||
"HeaderCameraUploads": "Kamera-Uploads",
|
||||
"HeaderContinueWatching": "Fortsetzen",
|
||||
"HeaderFavoriteAlbums": "Lieblingsalben",
|
||||
"HeaderFavoriteArtists": "Lieblings-Interpreten",
|
||||
|
@ -114,5 +113,7 @@
|
|||
"TasksChannelsCategory": "Internet Kanäle",
|
||||
"TasksApplicationCategory": "Anwendung",
|
||||
"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": "Φάκελοι",
|
||||
"Genres": "Είδη",
|
||||
"HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
|
||||
"HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
|
||||
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
|
||||
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
|
||||
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Folders",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Album Artists",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderContinueWatching": "Continue Watching",
|
||||
"HeaderFavoriteAlbums": "Favourite Albums",
|
||||
"HeaderFavoriteArtists": "Favourite Artists",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Folders",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Album Artists",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderContinueWatching": "Continue Watching",
|
||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
||||
"HeaderFavoriteArtists": "Favorite Artists",
|
||||
|
@ -96,6 +95,8 @@
|
|||
"TasksLibraryCategory": "Library",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"TasksChannelsCategory": "Internet Channels",
|
||||
"TaskCleanActivityLog": "Clean Activity Log",
|
||||
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
|
||||
"TaskCleanCache": "Clean Cache Directory",
|
||||
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
|
||||
"TaskRefreshChapterImages": "Extract Chapter Images",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Carpetas",
|
||||
"Genres": "Géneros",
|
||||
"HeaderAlbumArtists": "Artistas de álbum",
|
||||
"HeaderCameraUploads": "Subidas de cámara",
|
||||
"HeaderContinueWatching": "Seguir viendo",
|
||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Carpetas",
|
||||
"Genres": "Géneros",
|
||||
"HeaderAlbumArtists": "Artistas del álbum",
|
||||
"HeaderCameraUploads": "Subidas desde la cámara",
|
||||
"HeaderContinueWatching": "Continuar viendo",
|
||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Carpetas",
|
||||
"Genres": "Géneros",
|
||||
"HeaderAlbumArtists": "Artistas del álbum",
|
||||
"HeaderCameraUploads": "Subidas desde la cámara",
|
||||
"HeaderContinueWatching": "Continuar viendo",
|
||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
|
@ -78,7 +77,7 @@
|
|||
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
|
||||
"Sync": "Sincronizar",
|
||||
"System": "Sistema",
|
||||
"TvShows": "Programas de televisión",
|
||||
"TvShows": "Series",
|
||||
"User": "Usuario",
|
||||
"UserCreatedWithName": "El usuario {0} ha sido creado",
|
||||
"UserDeletedWithName": "El usuario {0} ha sido borrado",
|
||||
|
|
|
@ -105,7 +105,6 @@
|
|||
"Inherit": "Heredar",
|
||||
"HomeVideos": "Videos caseros",
|
||||
"HeaderRecordingGroups": "Grupos de grabación",
|
||||
"HeaderCameraUploads": "Subidas desde la cámara",
|
||||
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
|
||||
"DeviceOnlineWithName": "{0} está conectado",
|
||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"Application": "Aplicación",
|
||||
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
||||
"HeaderContinueWatching": "Continuar Viendo",
|
||||
"HeaderCameraUploads": "Subidas de Cámara",
|
||||
"HeaderAlbumArtists": "Artistas del Álbum",
|
||||
"Genres": "Géneros",
|
||||
"Folders": "Carpetas",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "پوشهها",
|
||||
"Genres": "ژانرها",
|
||||
"HeaderAlbumArtists": "هنرمندان آلبوم",
|
||||
"HeaderCameraUploads": "آپلودهای دوربین",
|
||||
"HeaderContinueWatching": "ادامه تماشا",
|
||||
"HeaderFavoriteAlbums": "آلبومهای مورد علاقه",
|
||||
"HeaderFavoriteArtists": "هنرمندان مورد علاقه",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"HeaderLiveTV": "Live-TV",
|
||||
"NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
|
||||
"NameSeasonUnknown": "Tuntematon Kausi",
|
||||
"NameSeasonUnknown": "Tuntematon kausi",
|
||||
"NameSeasonNumber": "Kausi {0}",
|
||||
"NameInstallFailed": "{0} asennus epäonnistui",
|
||||
"MusicVideos": "Musiikkivideot",
|
||||
|
@ -19,24 +19,23 @@
|
|||
"ItemAddedWithName": "{0} lisättiin kirjastoon",
|
||||
"Inherit": "Periytyä",
|
||||
"HomeVideos": "Kotivideot",
|
||||
"HeaderRecordingGroups": "Nauhoiteryhmät",
|
||||
"HeaderRecordingGroups": "Tallennusryhmät",
|
||||
"HeaderNextUp": "Seuraavaksi",
|
||||
"HeaderFavoriteSongs": "Lempikappaleet",
|
||||
"HeaderFavoriteShows": "Lempisarjat",
|
||||
"HeaderFavoriteEpisodes": "Lempijaksot",
|
||||
"HeaderCameraUploads": "Kamerasta Lähetetyt",
|
||||
"HeaderFavoriteArtists": "Lempiartistit",
|
||||
"HeaderFavoriteAlbums": "Lempialbumit",
|
||||
"HeaderFavoriteSongs": "Suosikkikappaleet",
|
||||
"HeaderFavoriteShows": "Suosikkisarjat",
|
||||
"HeaderFavoriteEpisodes": "Suosikkijaksot",
|
||||
"HeaderFavoriteArtists": "Suosikkiartistit",
|
||||
"HeaderFavoriteAlbums": "Suosikkialbumit",
|
||||
"HeaderContinueWatching": "Jatka katsomista",
|
||||
"HeaderAlbumArtists": "Albumin esittäjä",
|
||||
"HeaderAlbumArtists": "Albumin artistit",
|
||||
"Genres": "Tyylilajit",
|
||||
"Folders": "Kansiot",
|
||||
"Favorites": "Suosikit",
|
||||
"FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
|
||||
"DeviceOnlineWithName": "{0} on yhdistetty",
|
||||
"DeviceOfflineWithName": "{0} on katkaissut yhteytensä",
|
||||
"DeviceOfflineWithName": "{0} yhteys on katkaistu",
|
||||
"Collections": "Kokoelmat",
|
||||
"ChapterNameValue": "Luku: {0}",
|
||||
"ChapterNameValue": "Jakso: {0}",
|
||||
"Channels": "Kanavat",
|
||||
"CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
|
||||
"Books": "Kirjat",
|
||||
|
@ -62,25 +61,25 @@
|
|||
"UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}",
|
||||
"UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
|
||||
"UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
|
||||
"UserOfflineFromDevice": "{0} yhteys katkaistu {1}",
|
||||
"UserOfflineFromDevice": "{0} yhteys katkaistu kohteesta {1}",
|
||||
"UserLockedOutWithName": "Käyttäjä {0} lukittu",
|
||||
"UserDownloadingItemWithValues": "{0} lataa {1}",
|
||||
"UserDeletedWithName": "Käyttäjä {0} poistettu",
|
||||
"UserCreatedWithName": "Käyttäjä {0} luotu",
|
||||
"TvShows": "TV-sarjat",
|
||||
"TvShows": "TV-ohjelmat",
|
||||
"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",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.",
|
||||
"SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.",
|
||||
"Songs": "Kappaleet",
|
||||
"Shows": "Sarjat",
|
||||
"ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen",
|
||||
"Shows": "Ohjelmat",
|
||||
"ServerNameNeedsToBeRestarted": "{0} on käynnistettävä uudelleen",
|
||||
"ProviderValue": "Tarjoaja: {0}",
|
||||
"Plugin": "Liitännäinen",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
|
||||
"NotificationOptionVideoPlayback": "Videota toistetaan",
|
||||
"NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
|
||||
"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",
|
||||
"NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
|
||||
"NotificationOptionPluginInstalled": "Liitännäinen asennettu",
|
||||
|
@ -105,10 +104,10 @@
|
|||
"TaskRefreshPeople": "Päivitä henkilöt",
|
||||
"TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
|
||||
"TaskCleanLogs": "Puhdista lokihakemisto",
|
||||
"TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.",
|
||||
"TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.",
|
||||
"TaskRefreshLibrary": "Skannaa mediakirjasto",
|
||||
"TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.",
|
||||
"TaskRefreshChapterImages": "Eristä lukujen kuvat",
|
||||
"TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on jaksoja.",
|
||||
"TaskRefreshChapterImages": "Pura jakson kuvat",
|
||||
"TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
|
||||
"TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
|
||||
"TasksChannelsCategory": "Internet kanavat",
|
||||
|
|
|
@ -73,7 +73,6 @@
|
|||
"HeaderFavoriteArtists": "Paboritong Artista",
|
||||
"HeaderFavoriteAlbums": "Paboritong Albums",
|
||||
"HeaderContinueWatching": "Ituloy Manood",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderAlbumArtists": "Artista ng Album",
|
||||
"Genres": "Kategorya",
|
||||
"Folders": "Folders",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Dossiers",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Artistes de l'album",
|
||||
"HeaderCameraUploads": "Photos transférées",
|
||||
"HeaderContinueWatching": "Continuer à regarder",
|
||||
"HeaderFavoriteAlbums": "Albums favoris",
|
||||
"HeaderFavoriteArtists": "Artistes favoris",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Dossiers",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Artistes",
|
||||
"HeaderCameraUploads": "Photos transférées",
|
||||
"HeaderContinueWatching": "Continuer à regarder",
|
||||
"HeaderFavoriteAlbums": "Albums favoris",
|
||||
"HeaderFavoriteArtists": "Artistes préférés",
|
||||
|
@ -114,5 +113,7 @@
|
|||
"TaskCleanCache": "Vider le répertoire cache",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"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",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Album-Künstler",
|
||||
"HeaderCameraUploads": "Kamera-Uploads",
|
||||
"HeaderContinueWatching": "weiter schauen",
|
||||
"HeaderFavoriteAlbums": "Lieblingsalben",
|
||||
"HeaderFavoriteArtists": "Lieblings-Künstler",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "תיקיות",
|
||||
"Genres": "ז'אנרים",
|
||||
"HeaderAlbumArtists": "אמני האלבום",
|
||||
"HeaderCameraUploads": "העלאות ממצלמה",
|
||||
"HeaderContinueWatching": "המשך לצפות",
|
||||
"HeaderFavoriteAlbums": "אלבומים מועדפים",
|
||||
"HeaderFavoriteArtists": "אמנים מועדפים",
|
||||
|
|
3
Emby.Server.Implementations/Localization/Core/hi.json
Normal file
3
Emby.Server.Implementations/Localization/Core/hi.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"Albums": "आल्बुम्"
|
||||
}
|
|
@ -5,18 +5,17 @@
|
|||
"Artists": "Izvođači",
|
||||
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
|
||||
"Books": "Knjige",
|
||||
"CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
|
||||
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
|
||||
"Channels": "Kanali",
|
||||
"ChapterNameValue": "Poglavlje {0}",
|
||||
"Collections": "Kolekcije",
|
||||
"DeviceOfflineWithName": "{0} se odspojilo",
|
||||
"DeviceOnlineWithName": "{0} je spojeno",
|
||||
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
|
||||
"DeviceOfflineWithName": "{0} je prekinuo vezu",
|
||||
"DeviceOnlineWithName": "{0} je povezan",
|
||||
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
|
||||
"Favorites": "Favoriti",
|
||||
"Folders": "Mape",
|
||||
"Genres": "Žanrovi",
|
||||
"HeaderAlbumArtists": "Izvođači na albumu",
|
||||
"HeaderCameraUploads": "Uvoz sa kamere",
|
||||
"HeaderContinueWatching": "Nastavi gledati",
|
||||
"HeaderFavoriteAlbums": "Omiljeni albumi",
|
||||
"HeaderFavoriteArtists": "Omiljeni izvođači",
|
||||
|
@ -24,95 +23,97 @@
|
|||
"HeaderFavoriteShows": "Omiljene serije",
|
||||
"HeaderFavoriteSongs": "Omiljene pjesme",
|
||||
"HeaderLiveTV": "TV uživo",
|
||||
"HeaderNextUp": "Sljedeće je",
|
||||
"HeaderNextUp": "Slijedi",
|
||||
"HeaderRecordingGroups": "Grupa snimka",
|
||||
"HomeVideos": "Kućni videi",
|
||||
"HomeVideos": "Kućni video",
|
||||
"Inherit": "Naslijedi",
|
||||
"ItemAddedWithName": "{0} je dodano u biblioteku",
|
||||
"ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
|
||||
"ItemRemovedWithName": "{0} je uklonjeno iz biblioteke",
|
||||
"LabelIpAddressValue": "IP adresa: {0}",
|
||||
"LabelRunningTimeValue": "Vrijeme rada: {0}",
|
||||
"Latest": "Najnovije",
|
||||
"MessageApplicationUpdated": "Jellyfin Server je ažuriran",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
|
||||
"MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
|
||||
"MessageApplicationUpdated": "Jellyfin server je ažuriran",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran",
|
||||
"MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
|
||||
"MixedContent": "Miješani sadržaj",
|
||||
"Movies": "Filmovi",
|
||||
"Music": "Glazba",
|
||||
"MusicVideos": "Glazbeni spotovi",
|
||||
"NameInstallFailed": "{0} neuspješnih instalacija",
|
||||
"NameSeasonNumber": "Sezona {0}",
|
||||
"NameSeasonUnknown": "Nepoznata sezona",
|
||||
"NameSeasonUnknown": "Sezona nepoznata",
|
||||
"NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
|
||||
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
|
||||
"NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
|
||||
"NotificationOptionInstallationFailed": "Instalacija neuspješna",
|
||||
"NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
|
||||
"NotificationOptionPluginError": "Dodatak otkazao",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Dostupno je ažuriranje aplikacije",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Instalirano je ažuriranje aplikacije",
|
||||
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započela",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodukcija glazbe zaustavljena",
|
||||
"NotificationOptionCameraImageUploaded": "Slika s kamere učitana",
|
||||
"NotificationOptionInstallationFailed": "Instalacija nije uspjela",
|
||||
"NotificationOptionNewLibraryContent": "Novi sadržaj dodan",
|
||||
"NotificationOptionPluginError": "Dodatak zakazao",
|
||||
"NotificationOptionPluginInstalled": "Dodatak instaliran",
|
||||
"NotificationOptionPluginUninstalled": "Dodatak uklonjen",
|
||||
"NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje za dodatak",
|
||||
"NotificationOptionServerRestartRequired": "Potrebno ponovo pokretanje servera",
|
||||
"NotificationOptionTaskFailed": "Zakazan zadatak nije izvršen",
|
||||
"NotificationOptionPluginUninstalled": "Dodatak deinstaliran",
|
||||
"NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje dodatka",
|
||||
"NotificationOptionServerRestartRequired": "Ponovno pokrenite server",
|
||||
"NotificationOptionTaskFailed": "Greška zakazanog zadatka",
|
||||
"NotificationOptionUserLockedOut": "Korisnik zaključan",
|
||||
"NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
|
||||
"Photos": "Slike",
|
||||
"Playlists": "Popis za reprodukciju",
|
||||
"NotificationOptionVideoPlayback": "Reprodukcija videa započela",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena",
|
||||
"Photos": "Fotografije",
|
||||
"Playlists": "Popisi za reprodukciju",
|
||||
"Plugin": "Dodatak",
|
||||
"PluginInstalledWithName": "{0} je instalirano",
|
||||
"PluginUninstalledWithName": "{0} je deinstalirano",
|
||||
"PluginUpdatedWithName": "{0} je ažurirano",
|
||||
"ProviderValue": "Pružitelj: {0}",
|
||||
"ProviderValue": "Pružatelj: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} neuspjelo",
|
||||
"ScheduledTaskStartedWithName": "{0} pokrenuto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
|
||||
"Shows": "Serije",
|
||||
"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}",
|
||||
"SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}",
|
||||
"Sync": "Sink.",
|
||||
"System": "Sistem",
|
||||
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
|
||||
"Sync": "Sinkronizacija",
|
||||
"System": "Sustav",
|
||||
"TvShows": "Serije",
|
||||
"User": "Korisnik",
|
||||
"UserCreatedWithName": "Korisnik {0} je stvoren",
|
||||
"UserCreatedWithName": "Korisnik {0} je kreiran",
|
||||
"UserDeletedWithName": "Korisnik {0} je obrisan",
|
||||
"UserDownloadingItemWithValues": "{0} se preuzima {1}",
|
||||
"UserDownloadingItemWithValues": "{0} preuzima {1}",
|
||||
"UserLockedOutWithName": "Korisnik {0} je zaključan",
|
||||
"UserOfflineFromDevice": "{0} se odspojilo od {1}",
|
||||
"UserOnlineFromDevice": "{0} je online od {1}",
|
||||
"UserOfflineFromDevice": "{0} prekinuo vezu od {1}",
|
||||
"UserOnlineFromDevice": "{0} povezan od {1}",
|
||||
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
|
||||
"UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
|
||||
"UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
|
||||
"ValueSpecialEpisodeName": "Specijal - {0}",
|
||||
"ValueSpecialEpisodeName": "Posebno - {0}",
|
||||
"VersionNumber": "Verzija {0}",
|
||||
"TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
|
||||
"TaskRefreshLibrary": "Skeniraj medijsku knjižnicu",
|
||||
"TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.",
|
||||
"TaskRefreshChapterImages": "Raspakiraj slike poglavlja",
|
||||
"TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
|
||||
"TaskCleanCache": "Očisti priručnu memoriju",
|
||||
"TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
|
||||
"TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
|
||||
"TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
|
||||
"TaskRefreshChapterImages": "Izdvoji slike poglavlja",
|
||||
"TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
|
||||
"TaskCleanCache": "Očisti mapu predmemorije",
|
||||
"TasksApplicationCategory": "Aplikacija",
|
||||
"TasksMaintenanceCategory": "Održavanje",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.",
|
||||
"TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju",
|
||||
"TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
|
||||
"TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
|
||||
"TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
|
||||
"TaskRefreshChannels": "Osvježi kanale",
|
||||
"TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.",
|
||||
"TaskCleanTranscode": "Očisti direktorij za transkodiranje",
|
||||
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.",
|
||||
"TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
|
||||
"TaskCleanTranscode": "Očisti mapu transkodiranja",
|
||||
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
|
||||
"TaskUpdatePlugins": "Ažuriraj dodatke",
|
||||
"TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.",
|
||||
"TaskRefreshPeople": "Osvježi ljude",
|
||||
"TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.",
|
||||
"TaskCleanLogs": "Očisti direktorij sa logovima",
|
||||
"TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
|
||||
"TaskRefreshPeople": "Osvježi osobe",
|
||||
"TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
|
||||
"TaskCleanLogs": "Očisti mapu dnevnika zapisa",
|
||||
"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",
|
||||
"Genres": "Műfajok",
|
||||
"HeaderAlbumArtists": "Album előadók",
|
||||
"HeaderCameraUploads": "Kamera feltöltések",
|
||||
"HeaderContinueWatching": "Megtekintés folytatása",
|
||||
"HeaderFavoriteAlbums": "Kedvenc albumok",
|
||||
"HeaderFavoriteArtists": "Kedvenc előadók",
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
"HeaderFavoriteArtists": "Artis Favorit",
|
||||
"HeaderFavoriteAlbums": "Album Favorit",
|
||||
"HeaderContinueWatching": "Lanjut Menonton",
|
||||
"HeaderCameraUploads": "Unggahan Kamera",
|
||||
"HeaderAlbumArtists": "Album Artis",
|
||||
"Genres": "Aliran",
|
||||
"Folders": "Folder",
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"HeaderFavoriteArtists": "Uppáhalds Listamenn",
|
||||
"HeaderFavoriteAlbums": "Uppáhalds Plötur",
|
||||
"HeaderContinueWatching": "Halda áfram að horfa",
|
||||
"HeaderCameraUploads": "Myndavéla upphal",
|
||||
"HeaderAlbumArtists": "Höfundur plötu",
|
||||
"Genres": "Tegundir",
|
||||
"Folders": "Möppur",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Cartelle",
|
||||
"Genres": "Generi",
|
||||
"HeaderAlbumArtists": "Artisti degli Album",
|
||||
"HeaderCameraUploads": "Caricamenti Fotocamera",
|
||||
"HeaderContinueWatching": "Continua a guardare",
|
||||
"HeaderFavoriteAlbums": "Album Preferiti",
|
||||
"HeaderFavoriteArtists": "Artisti Preferiti",
|
||||
|
@ -114,5 +113,7 @@
|
|||
"TasksChannelsCategory": "Canali su Internet",
|
||||
"TasksApplicationCategory": "Applicazione",
|
||||
"TasksLibraryCategory": "Libreria",
|
||||
"TasksMaintenanceCategory": "Manutenzione"
|
||||
"TasksMaintenanceCategory": "Manutenzione",
|
||||
"TaskCleanActivityLog": "Attività di Registro Completate",
|
||||
"TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata."
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "フォルダー",
|
||||
"Genres": "ジャンル",
|
||||
"HeaderAlbumArtists": "アルバムアーティスト",
|
||||
"HeaderCameraUploads": "カメラアップロード",
|
||||
"HeaderContinueWatching": "視聴を続ける",
|
||||
"HeaderFavoriteAlbums": "お気に入りのアルバム",
|
||||
"HeaderFavoriteArtists": "お気に入りのアーティスト",
|
||||
|
@ -97,7 +96,7 @@
|
|||
"TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。",
|
||||
"TaskRefreshLibrary": "メディアライブラリのスキャン",
|
||||
"TaskCleanCacheDescription": "不要なキャッシュを消去します。",
|
||||
"TaskCleanCache": "キャッシュの掃除",
|
||||
"TaskCleanCache": "キャッシュを消去",
|
||||
"TasksChannelsCategory": "ネットチャンネル",
|
||||
"TasksApplicationCategory": "アプリケーション",
|
||||
"TasksLibraryCategory": "ライブラリ",
|
||||
|
@ -113,5 +112,7 @@
|
|||
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
|
||||
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
|
||||
"TaskRefreshChapterImages": "チャプター画像を抽出する",
|
||||
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする"
|
||||
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
|
||||
"TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
|
||||
"TaskCleanActivityLog": "アクティビティの履歴を消去"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Qaltalar",
|
||||
"Genres": "Janrlar",
|
||||
"HeaderAlbumArtists": "Álbom oryndaýshylary",
|
||||
"HeaderCameraUploads": "Kameradan júktelgender",
|
||||
"HeaderContinueWatching": "Qaraýdy jalǵastyrý",
|
||||
"HeaderFavoriteAlbums": "Tańdaýly álbomdar",
|
||||
"HeaderFavoriteArtists": "Tańdaýly oryndaýshylar",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "폴더",
|
||||
"Genres": "장르",
|
||||
"HeaderAlbumArtists": "앨범 아티스트",
|
||||
"HeaderCameraUploads": "카메라 업로드",
|
||||
"HeaderContinueWatching": "계속 시청하기",
|
||||
"HeaderFavoriteAlbums": "즐겨찾는 앨범",
|
||||
"HeaderFavoriteArtists": "즐겨찾는 아티스트",
|
||||
|
@ -28,7 +27,7 @@
|
|||
"HeaderRecordingGroups": "녹화 그룹",
|
||||
"HomeVideos": "홈 비디오",
|
||||
"Inherit": "상속",
|
||||
"ItemAddedWithName": "{0}가 라이브러리에 추가됨",
|
||||
"ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다",
|
||||
"ItemRemovedWithName": "{0}가 라이브러리에서 제거됨",
|
||||
"LabelIpAddressValue": "IP 주소: {0}",
|
||||
"LabelRunningTimeValue": "상영 시간: {0}",
|
||||
|
@ -114,5 +113,7 @@
|
|||
"TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.",
|
||||
"TaskCleanCache": "캐시 폴더 청소",
|
||||
"TasksChannelsCategory": "인터넷 채널",
|
||||
"TasksLibraryCategory": "라이브러리"
|
||||
"TasksLibraryCategory": "라이브러리",
|
||||
"TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.",
|
||||
"TaskCleanActivityLog": "활동내역청소"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Katalogai",
|
||||
"Genres": "Žanrai",
|
||||
"HeaderAlbumArtists": "Albumo atlikėjai",
|
||||
"HeaderCameraUploads": "Kameros",
|
||||
"HeaderContinueWatching": "Žiūrėti toliau",
|
||||
"HeaderFavoriteAlbums": "Mėgstami Albumai",
|
||||
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
|
||||
|
|
|
@ -72,7 +72,6 @@
|
|||
"ItemAddedWithName": "{0} tika pievienots bibliotēkai",
|
||||
"HeaderLiveTV": "Tiešraides TV",
|
||||
"HeaderContinueWatching": "Turpināt Skatīšanos",
|
||||
"HeaderCameraUploads": "Kameras augšupielādes",
|
||||
"HeaderAlbumArtists": "Albumu Izpildītāji",
|
||||
"Genres": "Žanri",
|
||||
"Folders": "Mapes",
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
"HeaderFavoriteArtists": "Омилени Изведувачи",
|
||||
"HeaderFavoriteAlbums": "Омилени Албуми",
|
||||
"HeaderContinueWatching": "Продолжи со гледање",
|
||||
"HeaderCameraUploads": "Поставувања од камера",
|
||||
"HeaderAlbumArtists": "Изведувачи од Албуми",
|
||||
"Genres": "Жанрови",
|
||||
"Folders": "Папки",
|
||||
|
|
|
@ -54,7 +54,6 @@
|
|||
"ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले",
|
||||
"HomeVideos": "घरचे व्हिडीयो",
|
||||
"HeaderRecordingGroups": "रेकॉर्डिंग गट",
|
||||
"HeaderCameraUploads": "कॅमेरा अपलोड",
|
||||
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
|
||||
"Application": "अॅप्लिकेशन",
|
||||
"AppDeviceValues": "अॅप: {0}, यंत्र: {1}",
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"Folders": "Fail-fail",
|
||||
"Genres": "Genre-genre",
|
||||
"HeaderAlbumArtists": "Album Artis-artis",
|
||||
"HeaderCameraUploads": "Muatnaik Kamera",
|
||||
"HeaderContinueWatching": "Terus Menonton",
|
||||
"HeaderFavoriteAlbums": "Album-album Kegemaran",
|
||||
"HeaderFavoriteArtists": "Artis-artis Kegemaran",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user