Merge branch 'jellyfin:master' into cast-with-localhost-server

pull/10415/head
Sky High 5 months ago committed by GitHub
commit c83432f9e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.0",
"version": "8.0.1",
"commands": [
"dotnet-ef"
]

@ -0,0 +1,28 @@
{
"name": "Development Jellyfin Server",
"image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
"postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust",
// reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
"dotnetRuntimeVersions": "8.0",
"aspNetCoreRuntimeVersions": "8.0"
},
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
"preserve_apt_list": false,
"packages": ["libfontconfig1"]
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"dockerDashComposeVersion": "v2"
},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
},
"hostRequirements": {
"memory": "8gb",
"cpus": 4
}
}

@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@c0d1daa7f7e14667747d73a7dbbe8c074bc8bfe2 # v2.22.9
uses: github/codeql-action/init@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@c0d1daa7f7e14667747d73a7dbbe8c074bc8bfe2 # v2.22.9
uses: github/codeql-action/autobuild@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c0d1daa7f7e14667747d73a7dbbe8c074bc8bfe2 # v2.22.9
uses: github/codeql-action/analyze@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0

@ -25,7 +25,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
with:
name: openapi-head
retention-days: 14
@ -59,7 +59,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
with:
name: openapi-base
retention-days: 14
@ -78,12 +78,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
with:
name: openapi-base
path: openapi-base

@ -19,9 +19,9 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
@ -34,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5
uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5.2.0
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
@ -42,9 +42,3 @@ jobs:
# TODO - which action / tool to use to publish code coverage results?
# - name: Publish code coverage results
- name: Publish OpenAPI Artifact
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
with:
name: "OpenAPI Spec"
path: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net*/openapi.json"

@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Remove from 'Current Release' project
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@ -24,7 +24,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true
with:
@ -33,7 +33,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@ -47,7 +47,7 @@ jobs:
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
- name: Move issue to needs triage
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true
with:
@ -56,7 +56,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true
with:

@ -1,13 +1,11 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig"
"editorconfig.editorconfig",
"GitHub.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.csdevkit"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [
]

@ -81,6 +81,7 @@
- [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00)
- [mitchfizz05](https://github.com/mitchfizz05)
- [mohd-akram](https://github.com/mohd-akram)
- [MrTimscampi](https://github.com/MrTimscampi)
- [n8225](https://github.com/n8225)
- [Nalsai](https://github.com/Nalsai)

@ -15,48 +15,48 @@
<PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.1.1" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.0" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.0" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.2.0" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
@ -70,21 +70,21 @@
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="1.0.0.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.1" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="TMDbLib" Version="2.1.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.6" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.6.1" />
<PackageVersion Include="xunit" Version="2.6.5" />
</ItemGroup>
</Project>

@ -76,6 +76,7 @@ using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.MediaEncoding.Transcoding;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
@ -583,7 +584,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
serviceCollection.AddSingleton<TranscodingJobHelper>();
serviceCollection.AddSingleton<ITranscodeManager, TranscodeManager>();
serviceCollection.AddScoped<MediaInfoHelper>();
serviceCollection.AddScoped<AudioHelper>();
serviceCollection.AddScoped<DynamicHlsHelper>();

@ -812,11 +812,16 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult is not null)
var jsonStream = AsyncFile.OpenRead(cachePath);
await using (jsonStream.ConfigureAwait(false))
{
return null;
var cachedResult = await JsonSerializer
.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
.ConfigureAwait(false);
if (cachedResult is not null)
{
return null;
}
}
}
}
@ -835,11 +840,16 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult is not null)
var jsonStream = AsyncFile.OpenRead(cachePath);
await using (jsonStream.ConfigureAwait(false))
{
return null;
var cachedResult = await JsonSerializer
.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
.ConfigureAwait(false);
if (cachedResult is not null)
{
return null;
}
}
}
}
@ -867,7 +877,7 @@ namespace Emby.Server.Implementations.Channels
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
}
await CacheResponse(result, cachePath);
await CacheResponse(result, cachePath).ConfigureAwait(false);
return result;
}
@ -883,8 +893,11 @@ namespace Emby.Server.Implementations.Channels
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
await using FileStream createStream = File.Create(path);
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
var createStream = File.Create(path);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
}
}
catch (Exception ex)
{

@ -104,6 +104,13 @@ namespace Emby.Server.Implementations.Data
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{
// If the resulting DateTimeKind is Unspecified it is actually Utc.
// This is required downstream for the Json serializer.
if (dateTimeResult.Kind == DateTimeKind.Unspecified)
{
dateTimeResult = DateTime.SpecifyKind(dateTimeResult, DateTimeKind.Utc);
}
result = dateTimeResult;
return true;
}

@ -418,15 +418,6 @@ namespace Emby.Server.Implementations.Dto
{
dto.PlayAccess = item.GetPlayAccess(user);
}
if (options.ContainsField(ItemFields.BasicSyncInfo))
{
var userCanSync = user is not null && user.HasPermission(PermissionKind.EnableContentDownloading);
if (userCanSync && item.SupportsExternalTransfer)
{
dto.SupportsSync = true;
}
}
}
private static int GetChildCount(Folder folder, User user)

@ -32,26 +32,26 @@ namespace Emby.Server.Implementations.Images
switch (viewType)
{
case CollectionType.Movies:
case CollectionType.movies:
includeItemTypes = new[] { BaseItemKind.Movie };
break;
case CollectionType.TvShows:
case CollectionType.tvshows:
includeItemTypes = new[] { BaseItemKind.Series };
break;
case CollectionType.Music:
case CollectionType.music:
includeItemTypes = new[] { BaseItemKind.MusicAlbum };
break;
case CollectionType.MusicVideos:
case CollectionType.musicvideos:
includeItemTypes = new[] { BaseItemKind.MusicVideo };
break;
case CollectionType.Books:
case CollectionType.books:
includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
break;
case CollectionType.BoxSets:
case CollectionType.boxsets:
includeItemTypes = new[] { BaseItemKind.BoxSet };
break;
case CollectionType.HomeVideos:
case CollectionType.Photos:
case CollectionType.homevideos:
case CollectionType.photos:
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
break;
default:
@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Images
break;
}
var recursive = viewType != CollectionType.Playlists;
var recursive = viewType != CollectionType.playlists;
return view.GetItemList(new InternalItemsQuery
{

@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images
var view = (UserView)item;
var isUsingCollectionStrip = IsUsingCollectionStrip(view);
var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.BoxSets && view.ViewType != CollectionType.Playlists;
var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.boxsets && view.ViewType != CollectionType.playlists;
var result = view.GetItemList(new InternalItemsQuery
{
@ -114,9 +114,9 @@ namespace Emby.Server.Implementations.Images
{
CollectionType[] collectionStripViewTypes =
{
CollectionType.Movies,
CollectionType.TvShows,
CollectionType.Playlists
CollectionType.movies,
CollectionType.tvshows,
CollectionType.playlists
};
return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value);

@ -12,7 +12,7 @@ using MediaBrowser.Model.Dto;
namespace Emby.Server.Implementations.Library
{
public class ExclusiveLiveStream : ILiveStream
public sealed class ExclusiveLiveStream : ILiveStream
{
private readonly Func<Task> _closeFn;
@ -51,5 +51,10 @@ namespace Emby.Server.Implementations.Library
{
return Task.CompletedTask;
}
/// <inheritdoc />
public void Dispose()
{
}
}
}

@ -1514,7 +1514,7 @@ namespace Emby.Server.Implementations.Library
{
if (item is UserView view)
{
if (view.ViewType == CollectionType.LiveTv)
if (view.ViewType == CollectionType.livetv)
{
return new[] { view.Id };
}
@ -1543,7 +1543,7 @@ namespace Emby.Server.Implementations.Library
}
// Handle grouping
if (user is not null && view.ViewType != CollectionType.Unknown && UserView.IsEligibleForGrouping(view.ViewType)
if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.ViewType)
&& user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
{
return GetUserRootFolder()

@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
List<FileSystemMetadata> files,
CollectionType? collectionType)
{
if (collectionType == CollectionType.Books)
if (collectionType == CollectionType.books)
{
return ResolveMultipleAudio(parent, files, true);
}
@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType();
var isBooksCollectionType = collectionType == CollectionType.Books;
var isBooksCollectionType = collectionType == CollectionType.books;
if (args.IsDirectory)
{
@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
MediaBrowser.Controller.Entities.Audio.Audio item = null;
var isMusicCollectionType = collectionType == CollectionType.Music;
var isMusicCollectionType = collectionType == CollectionType.music;
// Use regular audio type for mixed libraries, owned items and music
if (isMixedCollectionType ||

@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
protected override MusicAlbum Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
var isMusicMediaFolder = collectionType == CollectionType.Music;
var isMusicMediaFolder = collectionType == CollectionType.music;
// If there's a collection type and it's not music, don't allow it.
if (!isMusicMediaFolder)

@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType();
var isMusicMediaFolder = collectionType == CollectionType.Music;
var isMusicMediaFolder = collectionType == CollectionType.music;
// If there's a collection type and it's not music, it can't be a music artist
if (!isMusicMediaFolder)

@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var collectionType = args.GetCollectionType();
// Only process items that are in a collection folder containing books
if (collectionType != CollectionType.Books)
if (collectionType != CollectionType.books)
{
return null;
}

@ -31,11 +31,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private static readonly CollectionType[] _validCollectionTypes = new[]
{
CollectionType.Movies,
CollectionType.HomeVideos,
CollectionType.MusicVideos,
CollectionType.TvShows,
CollectionType.Photos
CollectionType.movies,
CollectionType.homevideos,
CollectionType.musicvideos,
CollectionType.tvshows,
CollectionType.photos
};
/// <summary>
@ -100,12 +100,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Video movie = null;
var files = args.GetActualFileSystemChildren().ToList();
if (collectionType == CollectionType.MusicVideos)
if (collectionType == CollectionType.musicvideos)
{
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
if (collectionType == CollectionType.HomeVideos)
if (collectionType == CollectionType.homevideos)
{
movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
if (collectionType == CollectionType.Movies)
if (collectionType == CollectionType.movies)
{
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
}
@ -147,17 +147,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Video item = null;
if (collectionType == CollectionType.MusicVideos)
if (collectionType == CollectionType.musicvideos)
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (collectionType == CollectionType.Movies)
else if (collectionType == CollectionType.movies)
{
item = ResolveVideo<Movie>(args, true);
}
else if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos)
else if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos)
{
item = ResolveVideo<Video>(args, false);
}
@ -195,12 +195,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
if (collectionType is CollectionType.MusicVideos)
if (collectionType is CollectionType.musicvideos)
{
return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
}
if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos)
if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos)
{
return ResolveVideos<Video>(parent, files, false, collectionType, false);
}
@ -221,12 +221,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return ResolveVideos<Movie>(parent, files, false, collectionType, true);
}
if (collectionType == CollectionType.Movies)
if (collectionType == CollectionType.movies)
{
return ResolveVideos<Movie>(parent, files, true, collectionType, true);
}
if (collectionType == CollectionType.TvShows)
if (collectionType == CollectionType.tvshows)
{
return ResolveVideos<Episode>(parent, files, false, collectionType, true);
}
@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var multiDiscFolders = new List<FileSystemMetadata>();
var libraryOptions = args.LibraryOptions;
var supportPhotos = collectionType == CollectionType.HomeVideos && libraryOptions.EnablePhotos;
var supportPhotos = collectionType == CollectionType.homevideos && libraryOptions.EnablePhotos;
var photos = new List<FileSystemMetadata>();
// Search for a folder rip
@ -459,7 +459,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult();
var isPhotosCollection = collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos;
var isPhotosCollection = collectionType == CollectionType.homevideos || collectionType == CollectionType.photos;
if (!isPhotosCollection && result.Items.Count == 1)
{
var videoPath = result.Items[0].Path;

@ -46,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection
var collectionType = args.GetCollectionType();
if (collectionType == CollectionType.Photos
|| (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos))
if (collectionType == CollectionType.photos
|| (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos))
{
if (HasPhotos(args))
{

@ -61,8 +61,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection
var collectionType = args.CollectionType;
if (collectionType == CollectionType.Photos
|| (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos))
if (collectionType == CollectionType.photos
|| (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos))
{
if (IsImageFile(args.Path, _imageProcessor))
{

@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
private CollectionType?[] _musicPlaylistCollectionTypes =
{
null,
CollectionType.Music
CollectionType.music
};
/// <inheritdoc/>

@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
if (season is not null
|| args.GetCollectionType() == CollectionType.TvShows
|| args.GetCollectionType() == CollectionType.tvshows
|| args.HasParent<Series>())
{
var episode = ResolveVideo<Episode>(args, false);

@ -60,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
var collectionType = args.GetCollectionType();
if (collectionType == CollectionType.TvShows)
if (collectionType == CollectionType.tvshows)
{
// TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
var configuredContentType = args.GetConfiguredContentType();
if (configuredContentType != CollectionType.TvShows)
if (configuredContentType != CollectionType.tvshows)
{
return new Series
{

@ -81,6 +81,53 @@ namespace Emby.Server.Implementations.Library
});
}
public void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason)
{
ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(item);
ArgumentNullException.ThrowIfNull(reason);
ArgumentNullException.ThrowIfNull(userDataDto);
var userData = GetUserData(user, item);
if (userDataDto.PlaybackPositionTicks.HasValue)
{
userData.PlaybackPositionTicks = userDataDto.PlaybackPositionTicks.Value;
}
if (userDataDto.PlayCount.HasValue)
{
userData.PlayCount = userDataDto.PlayCount.Value;
}
if (userDataDto.IsFavorite.HasValue)
{
userData.IsFavorite = userDataDto.IsFavorite.Value;
}
if (userDataDto.Likes.HasValue)
{
userData.Likes = userDataDto.Likes.Value;
}
if (userDataDto.Played.HasValue)
{
userData.Played = userDataDto.Played.Value;
}
if (userDataDto.LastPlayedDate.HasValue)
{
userData.LastPlayedDate = userDataDto.LastPlayedDate.Value;
}
if (userDataDto.Rating.HasValue)
{
userData.Rating = userDataDto.Rating.Value;
}
SaveUserData(user, item, userData, reason, CancellationToken.None);
}
/// <summary>
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
/// </summary>

@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library
var folderViewType = collectionFolder?.CollectionType;
// Playlist library requires special handling because the folder only references user playlists
if (folderViewType == CollectionType.Playlists)
if (folderViewType == CollectionType.playlists)
{
var items = folder.GetItemList(new InternalItemsQuery(user)
{
@ -99,14 +99,14 @@ namespace Emby.Server.Implementations.Library
}
}
foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows })
foreach (var viewType in new[] { CollectionType.movies, CollectionType.tvshows })
{
var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null)
.ToList();
if (parents.Count > 0)
{
var localizationKey = viewType == CollectionType.TvShows
var localizationKey = viewType == CollectionType.tvshows
? "TvShows"
: "Movies";
@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.Library
if (_config.Configuration.EnableFolderView)
{
var name = _localizationManager.GetLocalizedString("Folders");
list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty));
list.Add(_libraryManager.GetNamedView(name, CollectionType.folders, string.Empty));
}
if (query.IncludeExternalContent)
@ -279,7 +279,7 @@ namespace Emby.Server.Implementations.Library
var isPlayed = request.IsPlayed;
if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.Music))
if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.music))
{
isPlayed = null;
}
@ -305,11 +305,11 @@ namespace Emby.Server.Implementations.Library
var hasCollectionType = parents.OfType<UserView>().ToArray();
if (hasCollectionType.Length > 0)
{
if (hasCollectionType.All(i => i.CollectionType == CollectionType.Movies))
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
{
includeItemTypes = new[] { BaseItemKind.Movie };
}
else if (hasCollectionType.All(i => i.CollectionType == CollectionType.TvShows))
else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows))
{
includeItemTypes = new[] { BaseItemKind.Episode };
}
@ -324,18 +324,18 @@ namespace Emby.Server.Implementations.Library
{
switch (parent.CollectionType)
{
case CollectionType.Books:
case CollectionType.books:
mediaTypes.Add(MediaType.Book);
mediaTypes.Add(MediaType.Audio);
break;
case CollectionType.Music:
case CollectionType.music:
mediaTypes.Add(MediaType.Audio);
break;
case CollectionType.Photos:
case CollectionType.photos:
mediaTypes.Add(MediaType.Photo);
mediaTypes.Add(MediaType.Video);
break;
case CollectionType.HomeVideos:
case CollectionType.homevideos:
mediaTypes.Add(MediaType.Photo);
mediaTypes.Add(MediaType.Video);
break;

@ -8,13 +8,14 @@ using System.Threading.Tasks;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class DirectRecorder : IRecorder
public sealed class DirectRecorder : IRecorder
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
@ -46,7 +47,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
await using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
var output = new FileStream(
targetFile,
FileMode.CreateNew,
FileAccess.Write,
FileShare.Read,
IODefaults.FileStreamBufferSize,
FileOptions.Asynchronous);
await using (output.ConfigureAwait(false))
{
onStarted();
@ -80,24 +89,31 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
await using var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
await using (output.ConfigureAwait(false))
{
onStarted();
onStarted();
_logger.LogInformation("Copying recording stream to file {0}", targetFile);
_logger.LogInformation("Copying recording stream to file {0}", targetFile);
// The media source if infinite so we need to handle stopping ourselves
using var durationToken = new CancellationTokenSource(duration);
using var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
cancellationToken = linkedCancellationToken.Token;
// The media source if infinite so we need to handle stopping ourselves
using var durationToken = new CancellationTokenSource(duration);
using var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
cancellationToken = linkedCancellationToken.Token;
await _streamHelper.CopyUntilCancelled(
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
output,
IODefaults.CopyToBufferSize,
cancellationToken).ConfigureAwait(false);
await _streamHelper.CopyUntilCancelled(
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
output,
IODefaults.CopyToBufferSize,
cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Recording completed to file {0}", targetFile);
}
}
_logger.LogInformation("Recording completed to file {0}", targetFile);
/// <inheritdoc />
public void Dispose()
{
}
}
}

@ -37,12 +37,11 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
public sealed class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
{
public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
@ -74,7 +73,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
private bool _disposed = false;
private bool _disposed;
public EmbyTV(
IServerApplicationHost appHost,
@ -1270,7 +1269,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
directStreamProvider = liveStreamResponse.Item2;
}
var recorder = GetRecorder(mediaStreamInfo);
using var recorder = GetRecorder(mediaStreamInfo);
recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath);
recordPath = EnsureFileUnique(recordPath, timer.Id);
@ -2524,22 +2523,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_recordingDeleteSemaphore.Dispose();
}
_recordingDeleteSemaphore.Dispose();
foreach (var pair in _activeRecordings.ToList())
{

@ -25,7 +25,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class EncodedRecorder : IRecorder, IDisposable
public class EncodedRecorder : IRecorder
{
private readonly ILogger _logger;
private readonly IMediaEncoder _mediaEncoder;
@ -34,10 +34,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _hasExited;
private Stream _logFileStream;
private FileStream _logFileStream;
private string _targetPath;
private Process _process;
private bool _disposed = false;
private bool _disposed;
public EncodedRecorder(
ILogger logger,
@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
private async Task StartStreamingLog(Stream source, Stream target)
private async Task StartStreamingLog(Stream source, FileStream target)
{
try
{

@ -8,7 +8,7 @@ using MediaBrowser.Model.Dto;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public interface IRecorder
public interface IRecorder : IDisposable
{
/// <summary>
/// Records the specified media source.

@ -287,7 +287,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
IsMovie = IsMovie(details),
Etag = programInfo.Md5,
IsLive = string.Equals(programInfo.LiveTapeDelay, "live", StringComparison.OrdinalIgnoreCase),
IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1
IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).Contains("premiere", StringComparison.OrdinalIgnoreCase)
};
var showId = programId;
@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return null;
}
if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
if (uri.Contains("http", StringComparison.OrdinalIgnoreCase))
{
return uri;
}

@ -84,38 +84,53 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
}
}
else
{
await using var stream = AsyncFile.OpenRead(info.Path);
return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
var stream = AsyncFile.OpenRead(info.Path);
await using (stream.ConfigureAwait(false))
{
return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
}
}
}
private async Task<string> UnzipIfNeededAndCopy(string originalUrl, Stream stream, string file, CancellationToken cancellationToken)
{
await using var fileStream = new FileStream(file, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase))
var fileStream = new FileStream(
file,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
IODefaults.FileStreamBufferSize,
FileOptions.Asynchronous);
await using (fileStream.ConfigureAwait(false))
{
try
if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase))
{
using var reader = new GZipStream(stream, CompressionMode.Decompress);
await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
try
{
using var reader = new GZipStream(stream, CompressionMode.Decompress);
await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting from gz file {File}", originalUrl);
}
}
catch (Exception ex)
else
{
_logger.LogError(ex, "Error extracting from gz file {File}", originalUrl);
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
}
else
{
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
return file;
return file;
}
}
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)

@ -105,9 +105,9 @@ namespace Emby.Server.Implementations.LiveTv
/// <value>The services.</value>
public IReadOnlyList<ILiveTvService> Services => _services;
public ITunerHost[] TunerHosts => _tunerHosts;
public IReadOnlyList<ITunerHost> TunerHosts => _tunerHosts;
public IListingsProvider[] ListingProviders => _listingProviders;
public IReadOnlyList<IListingsProvider> ListingProviders => _listingProviders;
private LiveTvOptions GetConfiguration()
{
@ -1095,13 +1095,12 @@ namespace Emby.Server.Implementations.LiveTv
// Load these now which will prefetch metadata
var dtoOptions = new DtoOptions();
var fields = dtoOptions.Fields.ToList();
fields.Remove(ItemFields.BasicSyncInfo);
dtoOptions.Fields = fields.ToArray();
progress.Report(100);
}
private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, IProgress<double> progress, CancellationToken cancellationToken)
private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, ActionableProgress<double> progress, CancellationToken cancellationToken)
{
progress.Report(10);
@ -2168,7 +2167,7 @@ namespace Emby.Server.Implementations.LiveTv
public Folder GetInternalLiveTvFolder(CancellationToken cancellationToken)
{
var name = _localization.GetLocalizedString("HeaderLiveTV");
return _libraryManager.GetNamedView(name, CollectionType.LiveTv, name);
return _libraryManager.GetNamedView(name, CollectionType.livetv, name);
}
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)

@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return list;
}
protected virtual List<TunerHostInfo> GetTunerHosts()
protected virtual IList<TunerHostInfo> GetTunerHosts()
{
return GetConfiguration().TunerHosts
.Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
@ -96,8 +96,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
await using var writeStream = AsyncFile.OpenWrite(channelCacheFile);
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
var writeStream = AsyncFile.OpenWrite(channelCacheFile);
await using (writeStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
catch (IOException)
{
@ -112,10 +115,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
await using var readStream = AsyncFile.OpenRead(channelCacheFile);
var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
list.AddRange(channels);
var readStream = AsyncFile.OpenRead(channelCacheFile);
await using (readStream.ConfigureAwait(false))
{
var channels = await JsonSerializer
.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
list.AddRange(channels);
}
}
catch (IOException)
{
@ -159,9 +166,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return new List<MediaSourceInfo>();
}
protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(channelId);

@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = ModelNumber ?? string.Empty;
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
if (model.Contains("hdtc", StringComparison.OrdinalIgnoreCase))
{
return true;
}

@ -527,7 +527,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return list;
}
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = tunerHost.TunerCount;

@ -112,6 +112,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return stream;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool dispose)
{
if (dispose)
{
LiveStreamCancellationTokenSource?.Dispose();
}
}
protected async Task DeleteTempFiles(string path, int retryCount = 0)
{
if (retryCount == 0)
@ -134,7 +149,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
private void TrySeek(Stream stream, long offset)
private void TrySeek(FileStream stream, long offset)
{
if (!stream.CanSeek)
{

@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = tunerHost.TunerCount;

@ -66,7 +66,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStreamAsync(cancellationToken);
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
}
private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)

@ -83,14 +83,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
using (response)
{
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await StreamHelper.CopyToAsync(
stream,
fileStream,
IODefaults.CopyToBufferSize,
() => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
var fileStream = new FileStream(
TempFilePath,
FileMode.Create,
FileAccess.Write,
FileShare.Read,
IODefaults.FileStreamBufferSize,
FileOptions.Asynchronous);
await using (fileStream.ConfigureAwait(false))
{
await StreamHelper.CopyToAsync(
stream,
fileStream,
IODefaults.CopyToBufferSize,
() => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false);
}
}
}
}
catch (OperationCanceledException ex)

@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
"External": "Extern",
"HearingImpaired": "Discapacitat auditiva"
"HearingImpaired": "Discapacitat auditiva",
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
"TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades."
}

@ -83,8 +83,8 @@
"UserDeletedWithName": "Uživatel {0} byl smazán",
"UserDownloadingItemWithValues": "{0} stahuje {1}",
"UserLockedOutWithName": "Uživatel {0} byl odemčen",
"UserOfflineFromDevice": "{0} se odpojil od {1}",
"UserOnlineFromDevice": "{0} se připojil z {1}",
"UserOfflineFromDevice": "{0} se odpojil ze zařízení {1}",
"UserOnlineFromDevice": "{0} se připojil ze zařízení {1}",
"UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}",
"UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány",
"UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}",

@ -123,5 +123,7 @@
"External": "Externo",
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"HearingImpaired": "Discapacidad auditiva"
"HearingImpaired": "Discapacidad auditiva",
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción"
}

@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
"TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
"External": "خارجی",
"HearingImpaired": "مشکل شنوایی"
"HearingImpaired": "مشکل شنوایی",
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
"TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه."
}

@ -124,5 +124,6 @@
"TaskKeyframeExtractor": "Tagabunot ng Keyframe",
"TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
"External": "External",
"TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe"
"TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe",
"TaskRefreshTrickplayImagesDescription": "Nanggagawa ng mga trickplay prebiyu para sa mga bidyo sa pinaganang mga aklatan."
}

@ -0,0 +1,39 @@
{
"TasksLibraryCategory": "Գրադարան",
"TasksApplicationCategory": "Հավելված",
"TaskCleanActivityLog": "Մաքրել ակտիվության մատյանը",
"Application": "Հավելված",
"AuthenticationSucceededWithUserName": "{0} հաջողությամբ վավերականացվել են",
"Books": "Գրքեր",
"CameraImageUploadedFrom": "Նոր լուսանկար է վերբեռնվել {0}-ի կողմից",
"Channels": "Ալիքներ",
"DeviceOfflineWithName": "{0}ը անջատվեց",
"External": "Արտաքին",
"FailedLoginAttemptWithUserName": "Ձախողված մուտքի փործ {0}-ի կողմից",
"Folders": "Պանակներ",
"HeaderContinueWatching": "Շարունակել դիտումը",
"Inherit": "Ժառանգել",
"ItemAddedWithName": "{0}ը ավացված է գրադարանի մեջ",
"ItemRemovedWithName": "{0}ը հեռացված է գրադարանից",
"LabelIpAddressValue": "IP հասցե` {0}",
"Movies": "Ֆիլմեր",
"Music": "Երաժշտություն",
"NameSeasonNumber": "Սեզոն {0}",
"Photos": "Լուսանկարներ",
"PluginInstalledWithName": "{0}ն տեղադրված է",
"Songs": "Երգեր",
"System": "Համակարգ",
"TvShows": "Հեռուստասերիալներ",
"User": "Օգտատեր",
"VersionNumber": "Տարբերակ {0}",
"TasksMaintenanceCategory": "Սպասարկում",
"TasksChannelsCategory": "Ինտերնետային ալիքներ",
"TaskRefreshPeople": "Թարմացնել մարդկանց",
"TaskRefreshChannels": "Թարմացնել ալիքները",
"TaskDownloadMissingSubtitles": "Ներբեռնել պակասող ենթագրերը",
"Albums": "Ալբոմներ",
"AppDeviceValues": "Հավելված` {0}, Սարք `{1}",
"ChapterNameValue": "Գլուխ {0}",
"Collections": "Հավաքածուներ",
"DeviceOnlineWithName": "{0}-ն միացված է"
}

@ -123,5 +123,7 @@
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
"TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.",
"TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს."
"TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.",
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება"
}

@ -123,5 +123,7 @@
"External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem",
"TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs.",
"TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus",
"TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās."
}

@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.",
"TaskKeyframeExtractor": "Nøkkelbilde-uttrekker",
"External": "Ekstern",
"HearingImpaired": "Hørselshemmet"
"HearingImpaired": "Hørselshemmet",
"TaskRefreshTrickplayImages": "Generer Trickplay bilder",
"TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker."
}

@ -117,5 +117,6 @@
"TaskCleanActivityLog": "Slett aktivitetslogg",
"Undefined": "Udefinert",
"Forced": "Tvungen",
"Default": "Standard"
"Default": "Standard",
"External": "Ekstern"
}

@ -38,5 +38,18 @@
"HeaderFavoriteSongs": "ఇష్టమైన పాటలు",
"HeaderLiveTV": "ప్రత్యక్ష TV",
"HeaderNextUp": "తదుపరి",
"HeaderRecordingGroups": "రికార్డింగ్ గుంపులు"
"HeaderRecordingGroups": "రికార్డింగ్ గుంపులు",
"MessageApplicationUpdated": "జెల్లీఫిన్ సర్వర్ అప్‌డేట్ చేయడం పూర్తి అయ్యింది",
"MessageApplicationUpdatedTo": "జెల్లీఫిన్ సర్వర్ {0} వెర్షన్ కి అప్‌డేట్ చెయ్యబడింది",
"MessageServerConfigurationUpdated": "సర్వర్ కన్ఫిగరేషన్ అప్డేట్ చేయబడింది",
"NewVersionIsAvailable": "జెల్లీఫిన్ సర్వర్ యొక్క కొత్త వెర్షన్ డౌన్‌లోడ్ చేసుకోవడానికి అందుబాటులో ఉంది.",
"NotificationOptionApplicationUpdateInstalled": "అప్లికేషన్ అప్‌డేట్ ఇన్‌స్టాల్ చేయబడింది",
"ItemAddedWithName": "{0} లైబ్రరీకి జోడించబడింది",
"ItemRemovedWithName": "లైబ్రరీ నుండి {0} తీసివేయబడింది",
"LabelIpAddressValue": "ఐపీ చిరునామా: {0}",
"LabelRunningTimeValue": "నడుస్తున్న సమయం: {0}",
"Latest": "తాజా",
"NameInstallFailed": "{0} ఇన్‌స్టాలేషన్ విఫలమైంది",
"NameSeasonUnknown": "భాగం తెలియదు",
"NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్‌డేట్ అందుబాటులో ఉంది"
}

@ -89,7 +89,7 @@
"UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi",
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
"ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
"ValueHasBeenAddedToLibrary": "{0} medya kütüphanenize eklendi",
"ValueSpecialEpisodeName": "Özel - {0}",
"VersionNumber": "Sürüm {0}",
"TaskCleanCache": "Önbellek Dizinini Temizle",
@ -111,7 +111,7 @@
"TaskCleanLogs": "Günlük Dizinini Temizle",
"TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve ortam bilgilerini yeniler.",
"TaskRefreshLibrary": "Medya Kütüphanesini Tara",
"TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
"TaskRefreshChapterImagesDescription": "Bölümlere ayrılmış videolar için küçük resimler oluştur.",
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
"TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle",

@ -18,16 +18,16 @@
"HeaderContinueWatching": "Продовжити перегляд",
"HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри",
"Folders": "Каталоги",
"Folders": "Теки",
"Favorites": "Обрані",
"DeviceOnlineWithName": "Пристрій {0} підключився",
"DeviceOfflineWithName": "Пристрій {0} відключився",
"Collections": "Добірки",
"ChapterNameValue": "Розділ {0}",
"Collections": "Колекції",
"ChapterNameValue": "Сцена {0}",
"Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
"CameraImageUploadedFrom": "Нову фотографію завантажено з {0}",
"Books": "Книги",
"AuthenticationSucceededWithUserName": "{0} успішно автентифіковано",
"AuthenticationSucceededWithUserName": "{0} успішно авторизовано",
"Artists": "Виконавці",
"Application": "Додаток",
"AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
@ -83,7 +83,7 @@
"SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.",
"Songs": "Пісні",
"Shows": "Шоу",
"Shows": "Телепередачі",
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
"ScheduledTaskStartedWithName": "{0} розпочато",
"ScheduledTaskFailedWithName": "{0} незавершено, збій",

@ -696,7 +696,7 @@
"TwoLetterISORegionName": "SI"
},
{
"DisplayName": "Soomaaliya",
"DisplayName": "Somalia",
"Name": "SO",
"ThreeLetterISORegionName": "SOM",
"TwoLetterISORegionName": "SO"

@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Playlists
public override bool SupportsInheritedParentImages => false;
[JsonIgnore]
public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.Playlists;
public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.playlists;
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{

@ -1670,7 +1670,6 @@ namespace Emby.Server.Implementations.Session
var fields = dtoOptions.Fields.ToList();
fields.Remove(ItemFields.BasicSyncInfo);
fields.Remove(ItemFields.CanDelete);
fields.Remove(ItemFields.CanDownload);
fields.Remove(ItemFields.ChildCount);

@ -6,6 +6,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

@ -42,16 +42,15 @@ public class DevicesController : BaseJellyfinApiController
/// <summary>
/// Get Devices.
/// </summary>
/// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
/// <param name="userId">Gets or sets the user identifier.</param>
/// <response code="200">Devices retrieved.</response>
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
}
/// <summary>

@ -9,8 +9,8 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@ -19,6 +19,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
@ -51,7 +52,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ITranscodeManager _transcodeManager;
private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper;
private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator;
@ -67,7 +68,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
@ -79,7 +80,7 @@ public class DynamicHlsController : BaseJellyfinApiController
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
TranscodingJobHelper transcodingJobHelper,
ITranscodeManager transcodeManager,
ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper,
EncodingHelper encodingHelper,
@ -91,7 +92,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
_transcodingJobHelper = transcodingJobHelper;
_transcodeManager = transcodeManager;
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
_encodingHelper = encodingHelper;
@ -283,17 +284,17 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
_transcodingJobHelper,
_transcodeManager,
TranscodingJobType,
cancellationToken)
.ConfigureAwait(false);
TranscodingJobDto? job = null;
TranscodingJob? job = null;
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
if (!System.IO.File.Exists(playlistPath))
{
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
@ -302,11 +303,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// If the playlist doesn't already exist, startup ffmpeg
try
{
job = await _transcodingJobHelper.StartFfMpeg(
job = await _transcodeManager.StartFfMpeg(
state,
playlistPath,
GetCommandLineArguments(playlistPath, state, true, 0),
Request,
Request.HttpContext.User.GetUserId(),
TranscodingJobType,
cancellationTokenSource)
.ConfigureAwait(false);
@ -331,11 +332,11 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job is not null)
{
_transcodingJobHelper.OnTranscodeEndRequest(job);
_transcodeManager.OnTranscodeEndRequest(job);
}
var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
@ -1383,7 +1384,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
_transcodingJobHelper,
_transcodeManager,
TranscodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@ -1421,7 +1422,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
_transcodingJobHelper,
_transcodeManager,
TranscodingJobType,
cancellationToken)
.ConfigureAwait(false);
@ -1432,16 +1433,16 @@ public class DynamicHlsController : BaseJellyfinApiController
var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
TranscodingJobDto? job;
TranscodingJob? job;
if (System.IO.File.Exists(segmentPath))
{
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
_logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
var released = false;
var startTranscoding = false;
@ -1450,7 +1451,7 @@ public class DynamicHlsController : BaseJellyfinApiController
{
if (System.IO.File.Exists(segmentPath))
{
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release();
released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
@ -1488,7 +1489,7 @@ public class DynamicHlsController : BaseJellyfinApiController
// If the playlist doesn't already exist, startup ffmpeg
try
{
await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
.ConfigureAwait(false);
if (currentTranscodingIndex.HasValue)
@ -1499,11 +1500,11 @@ public class DynamicHlsController : BaseJellyfinApiController
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
state.WaitForPath = segmentPath;
job = await _transcodingJobHelper.StartFfMpeg(
job = await _transcodeManager.StartFfMpeg(
state,
playlistPath,
GetCommandLineArguments(playlistPath, state, false, segmentId),
Request,
Request.HttpContext.User.GetUserId(),
TranscodingJobType,
cancellationTokenSource).ConfigureAwait(false);
}
@ -1517,7 +1518,7 @@ public class DynamicHlsController : BaseJellyfinApiController
}
else
{
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job?.TranscodingThrottler is not null)
{
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
@ -1534,7 +1535,7 @@ public class DynamicHlsController : BaseJellyfinApiController
}
_logger.LogDebug("returning {0} [general case]", segmentPath);
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@ -1922,7 +1923,7 @@ public class DynamicHlsController : BaseJellyfinApiController
string segmentPath,
string segmentExtension,
int segmentIndex,
TranscodingJobDto? transcodingJob,
TranscodingJob? transcodingJob,
CancellationToken cancellationToken)
{
var segmentExists = System.IO.File.Exists(segmentPath);
@ -1991,7 +1992,7 @@ public class DynamicHlsController : BaseJellyfinApiController
return GetSegmentResult(state, segmentPath, transcodingJob);
}
private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob)
private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJob? transcodingJob)
{
var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks;
@ -2001,7 +2002,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (transcodingJob is not null)
{
transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
_transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
_transcodeManager.OnTranscodeEndRequest(transcodingJob);
}
return Task.CompletedTask;
@ -2012,7 +2013,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{
var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
var job = _transcodeManager.GetTranscodingJob(playlist, TranscodingJobType);
if (job is null || job.HasExited)
{

@ -131,8 +131,8 @@ public class GenresController : BaseJellyfinApiController
QueryResult<(BaseItem, ItemCounts)> result;
if (parentItem is ICollectionFolder parentCollectionFolder
&& (parentCollectionFolder.CollectionType == CollectionType.Music
|| parentCollectionFolder.CollectionType == CollectionType.MusicVideos))
&& (parentCollectionFolder.CollectionType == CollectionType.music
|| parentCollectionFolder.CollectionType == CollectionType.musicvideos))
{
result = _libraryManager.GetMusicGenres(query);
}

@ -24,22 +24,22 @@ public class HlsSegmentController : BaseJellyfinApiController
{
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ITranscodeManager _transcodeManager;
/// <summary>
/// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
public HlsSegmentController(
IFileSystem fileSystem,
IServerConfigurationManager serverConfigurationManager,
TranscodingJobHelper transcodingJobHelper)
ITranscodeManager transcodeManager)
{
_fileSystem = fileSystem;
_serverConfigurationManager = serverConfigurationManager;
_transcodingJobHelper = transcodingJobHelper;
_transcodeManager = transcodeManager;
}
/// <summary>
@ -112,7 +112,7 @@ public class HlsSegmentController : BaseJellyfinApiController
[FromQuery, Required] string deviceId,
[FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
_transcodeManager.KillTranscodingJobs(deviceId, playSessionId, _ => true);
return NoContent();
}
@ -174,13 +174,13 @@ public class HlsSegmentController : BaseJellyfinApiController
private ActionResult GetFileResult(string path, string playlistPath)
{
var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
Response.OnCompleted(() =>
{
if (transcodingJob is not null)
{
_transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
_transcodeManager.OnTranscodeEndRequest(transcodingJob);
}
return Task.CompletedTask;

@ -171,7 +171,7 @@ public class ItemUpdateController : BaseJellyfinApiController
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
info.ContentType = configuredContentType;
if (inheritedContentType is null || inheritedContentType == CollectionType.TvShows)
if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows)
{
info.ContentTypeOptions = info.ContentTypeOptions
.Where(i => string.IsNullOrWhiteSpace(i.Value)

@ -34,6 +34,7 @@ public class ItemsController : BaseJellyfinApiController
private readonly IDtoService _dtoService;
private readonly ILogger<ItemsController> _logger;
private readonly ISessionManager _sessionManager;
private readonly IUserDataManager _userDataRepository;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsController"/> class.
@ -44,13 +45,15 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
public ItemsController(
IUserManager userManager,
ILibraryManager libraryManager,
ILocalizationManager localization,
IDtoService dtoService,
ILogger<ItemsController> logger,
ISessionManager sessionManager)
ISessionManager sessionManager,
IUserDataManager userDataRepository)
{
_userManager = userManager;
_libraryManager = libraryManager;
@ -58,6 +61,7 @@ public class ItemsController : BaseJellyfinApiController
_dtoService = dtoService;
_logger = logger;
_sessionManager = sessionManager;
_userDataRepository = userDataRepository;
}
/// <summary>
@ -275,7 +279,7 @@ public class ItemsController : BaseJellyfinApiController
collectionType = hasCollectionType.CollectionType;
}
if (collectionType == CollectionType.Playlists)
if (collectionType == CollectionType.playlists)
{
recursive = true;
includeItemTypes = new[] { BaseItemKind.Playlist };
@ -881,4 +885,64 @@ public class ItemsController : BaseJellyfinApiController
itemsResult.TotalRecordCount,
returnItems);
}
/// <summary>
/// Get Item User Data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <response code="200">return item user data.</response>
/// <response code="404">Item is not found.</response>
/// <returns>Return <see cref="UserItemDataDto"/>.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> GetItemUserData(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
{
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data.");
}
var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById(itemId);
return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user);
}
/// <summary>
/// Update Item User Data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="userDataDto">New user data object.</param>
/// <response code="200">return updated user item data.</response>
/// <response code="404">Item is not found.</response>
/// <returns>Return <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/Items/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> UpdateItemUserData(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto)
{
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data.");
}
var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
_userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData);
return _userDataRepository.GetUserDataDto(item, user);
}
}

@ -927,15 +927,15 @@ public class LibraryController : BaseJellyfinApiController
{
return contentType switch
{
CollectionType.BoxSets => new[] { "BoxSet" },
CollectionType.Playlists => new[] { "Playlist" },
CollectionType.Movies => new[] { "Movie" },
CollectionType.TvShows => new[] { "Series", "Season", "Episode" },
CollectionType.Books => new[] { "Book" },
CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
CollectionType.HomeVideos => new[] { "Video", "Photo" },
CollectionType.Photos => new[] { "Video", "Photo" },
CollectionType.MusicVideos => new[] { "MusicVideo" },
CollectionType.boxsets => new[] { "BoxSet" },
CollectionType.playlists => new[] { "Playlist" },
CollectionType.movies => new[] { "Movie" },
CollectionType.tvshows => new[] { "Series", "Season", "Episode" },
CollectionType.books => new[] { "Book" },
CollectionType.music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
CollectionType.homevideos => new[] { "Video", "Photo" },
CollectionType.photos => new[] { "Video", "Photo" },
CollectionType.musicvideos => new[] { "MusicVideo" },
_ => new[] { "Series", "Season", "Episode", "Movie" }
};
}

@ -24,6 +24,8 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@ -47,7 +49,7 @@ public class LiveTvController : BaseJellyfinApiController
private readonly IDtoService _dtoService;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _configurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ITranscodeManager _transcodeManager;
/// <summary>
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
@ -59,7 +61,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
public LiveTvController(
ILiveTvManager liveTvManager,
IUserManager userManager,
@ -68,7 +70,7 @@ public class LiveTvController : BaseJellyfinApiController
IDtoService dtoService,
IMediaSourceManager mediaSourceManager,
IConfigurationManager configurationManager,
TranscodingJobHelper transcodingJobHelper)
ITranscodeManager transcodeManager)
{
_liveTvManager = liveTvManager;
_userManager = userManager;
@ -77,7 +79,7 @@ public class LiveTvController : BaseJellyfinApiController
_dtoService = dtoService;
_mediaSourceManager = mediaSourceManager;
_configurationManager = configurationManager;
_transcodingJobHelper = transcodingJobHelper;
_transcodeManager = transcodeManager;
}
/// <summary>
@ -1171,7 +1173,7 @@ public class LiveTvController : BaseJellyfinApiController
return NotFound();
}
var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper);
var stream = new ProgressiveFileStream(path, null, _transcodeManager);
return new FileStreamResult(stream, MimeTypes.GetMimeType(path));
}

@ -8,6 +8,7 @@ using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
@ -30,7 +31,7 @@ public class PlaystateController : BaseJellyfinApiController
private readonly ILibraryManager _libraryManager;
private readonly ISessionManager _sessionManager;
private readonly ILogger<PlaystateController> _logger;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ITranscodeManager _transcodeManager;
/// <summary>
/// Initializes a new instance of the <see cref="PlaystateController"/> class.
@ -40,14 +41,14 @@ public class PlaystateController : BaseJellyfinApiController
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param>
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
public PlaystateController(
IUserManager userManager,
IUserDataManager userDataRepository,
ILibraryManager libraryManager,
ISessionManager sessionManager,
ILoggerFactory loggerFactory,
TranscodingJobHelper transcodingJobHelper)
ITranscodeManager transcodeManager)
{
_userManager = userManager;
_userDataRepository = userDataRepository;
@ -55,7 +56,7 @@ public class PlaystateController : BaseJellyfinApiController
_sessionManager = sessionManager;
_logger = loggerFactory.CreateLogger<PlaystateController>();
_transcodingJobHelper = transcodingJobHelper;
_transcodeManager = transcodeManager;
}
/// <summary>
@ -188,7 +189,7 @@ public class PlaystateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
_transcodeManager.PingTranscodingJob(playSessionId, null);
return NoContent();
}
@ -205,7 +206,7 @@ public class PlaystateController : BaseJellyfinApiController
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
await _transcodeManager.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
@ -354,7 +355,7 @@ public class PlaystateController : BaseJellyfinApiController
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
await _transcodeManager.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
@ -388,7 +389,7 @@ public class PlaystateController : BaseJellyfinApiController
{
if (method == PlayMethod.Transcode)
{
var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId);
var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodeManager.GetTranscodingJob(playSessionId);
if (job is null)
{
return PlayMethod.DirectPlay;

@ -385,7 +385,6 @@ public class SessionController : BaseJellyfinApiController
/// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param>
/// <param name="supportedCommands">A list of supported remote control commands, comma delimited.</param>
/// <param name="supportsMediaControl">Determines whether media can be played remotely..</param>
/// <param name="supportsSync">Determines whether sync is supported.</param>
/// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param>
/// <response code="204">Capabilities posted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
@ -397,7 +396,6 @@ public class SessionController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
[FromQuery] bool supportsPersistentIdentifier = true)
{
if (string.IsNullOrWhiteSpace(id))
@ -410,7 +408,6 @@ public class SessionController : BaseJellyfinApiController
PlayableMediaTypes = playableMediaTypes,
SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync,
SupportsPersistentIdentifier = supportsPersistentIdentifier
});
return NoContent();

@ -11,6 +11,7 @@ using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization;

@ -90,7 +90,6 @@ public class UserViewsController : BaseJellyfinApiController
fields.Add(ItemFields.PrimaryImageAspectRatio);
fields.Add(ItemFields.DisplayPreferencesId);
fields.Remove(ItemFields.BasicSyncInfo);
dtoOptions.Fields = fields.ToArray();
var user = _userManager.GetUserById(userId);

@ -11,7 +11,6 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
@ -20,6 +19,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@ -43,7 +43,7 @@ public class VideosController : BaseJellyfinApiController
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ITranscodeManager _transcodeManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly EncodingHelper _encodingHelper;
@ -58,7 +58,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
public VideosController(
@ -68,7 +68,7 @@ public class VideosController : BaseJellyfinApiController
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
TranscodingJobHelper transcodingJobHelper,
ITranscodeManager transcodeManager,
IHttpClientFactory httpClientFactory,
EncodingHelper encodingHelper)
{
@ -78,7 +78,7 @@ public class VideosController : BaseJellyfinApiController
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_transcodingJobHelper = transcodingJobHelper;
_transcodeManager = transcodeManager;
_httpClientFactory = httpClientFactory;
_encodingHelper = encodingHelper;
}
@ -427,7 +427,7 @@ public class VideosController : BaseJellyfinApiController
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
_transcodingJobHelper,
_transcodeManager,
_transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@ -466,7 +466,7 @@ public class VideosController : BaseJellyfinApiController
if (state.MediaSource.IsInfiniteStream)
{
var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodeManager);
return File(liveStream, contentType);
}
@ -482,7 +482,7 @@ public class VideosController : BaseJellyfinApiController
state,
isHeadRequest,
HttpContext,
_transcodingJobHelper,
_transcodeManager,
ffmpegCommandLineArguments,
_transcodingJobType,
cancellationTokenSource).ConfigureAwait(false);

@ -2,13 +2,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
@ -26,7 +26,7 @@ public class AudioHelper
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ITranscodeManager _transcodeManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly EncodingHelper _encodingHelper;
@ -39,7 +39,7 @@ public class AudioHelper
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
/// <param name="transcodeManager">Instance of <see cref="ITranscodeManager"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
@ -49,7 +49,7 @@ public class AudioHelper
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
TranscodingJobHelper transcodingJobHelper,
ITranscodeManager transcodeManager,
IHttpClientFactory httpClientFactory,
IHttpContextAccessor httpContextAccessor,
EncodingHelper encodingHelper)
@ -59,7 +59,7 @@ public class AudioHelper
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_transcodingJobHelper = transcodingJobHelper;
_transcodeManager = transcodeManager;
_httpClientFactory = httpClientFactory;
_httpContextAccessor = httpContextAccessor;
_encodingHelper = encodingHelper;
@ -94,7 +94,7 @@ public class AudioHelper
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
_transcodingJobHelper,
_transcodeManager,
transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@ -133,7 +133,7 @@ public class AudioHelper
if (state.MediaSource.IsInfiniteStream)
{
var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodeManager);
return new FileStreamResult(stream, contentType);
}
@ -149,7 +149,7 @@ public class AudioHelper
state,
isHeadRequest,
_httpContextAccessor.HttpContext,
_transcodingJobHelper,
_transcodeManager,
ffmpegCommandLineArguments,
transcodingJobType,
cancellationTokenSource).ConfigureAwait(false);

@ -8,7 +8,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@ -18,6 +17,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
@ -39,7 +39,7 @@ public class DynamicHlsHelper
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ITranscodeManager _transcodeManager;
private readonly INetworkManager _networkManager;
private readonly ILogger<DynamicHlsHelper> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
@ -54,7 +54,7 @@ public class DynamicHlsHelper
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
/// <param name="transcodeManager">Instance of <see cref="ITranscodeManager"/>.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
@ -66,7 +66,7 @@ public class DynamicHlsHelper
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
TranscodingJobHelper transcodingJobHelper,
ITranscodeManager transcodeManager,
INetworkManager networkManager,
ILogger<DynamicHlsHelper> logger,
IHttpContextAccessor httpContextAccessor,
@ -78,7 +78,7 @@ public class DynamicHlsHelper
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_transcodingJobHelper = transcodingJobHelper;
_transcodeManager = transcodeManager;
_networkManager = networkManager;
_logger = logger;
_httpContextAccessor = httpContextAccessor;
@ -130,7 +130,7 @@ public class DynamicHlsHelper
_serverConfigurationManager,
_mediaEncoder,
_encodingHelper,
_transcodingJobHelper,
_transcodeManager,
transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);

@ -4,9 +4,9 @@ using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Api.Extensions;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
@ -65,7 +65,7 @@ public static class FileStreamResponseHelpers
/// <param name="state">The current <see cref="StreamState"/>.</param>
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
/// <param name="httpContext">The current http context.</param>
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
/// <param name="transcodeManager">The <see cref="ITranscodeManager"/> singleton.</param>
/// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
@ -74,7 +74,7 @@ public static class FileStreamResponseHelpers
StreamState state,
bool isHeadRequest,
HttpContext httpContext,
TranscodingJobHelper transcodingJobHelper,
ITranscodeManager transcodeManager,
string ffmpegCommandLineArguments,
TranscodingJobType transcodingJobType,
CancellationTokenSource cancellationTokenSource)
@ -93,22 +93,28 @@ public static class FileStreamResponseHelpers
return new OkResult();
}
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
var transcodingLock = transcodeManager.GetTranscodingLock(outputPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
TranscodingJobDto? job;
TranscodingJob? job;
if (!File.Exists(outputPath))
{
job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
job = await transcodeManager.StartFfMpeg(
state,
outputPath,
ffmpegCommandLineArguments,
httpContext.User.GetUserId(),
transcodingJobType,
cancellationTokenSource).ConfigureAwait(false);
}
else
{
job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
job = transcodeManager.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
state.Dispose();
}
var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
var stream = new ProgressiveFileStream(outputPath, job, transcodeManager);
return new FileStreamResult(stream, contentType);
}
finally

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;

@ -6,7 +6,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
@ -14,6 +13,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@ -38,7 +38,7 @@ public static class StreamingHelpers
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
/// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param>
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>
@ -51,7 +51,7 @@ public static class StreamingHelpers
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
EncodingHelper encodingHelper,
TranscodingJobHelper transcodingJobHelper,
ITranscodeManager transcodeManager,
TranscodingJobType transcodingJobType,
CancellationToken cancellationToken)
{
@ -74,7 +74,7 @@ public static class StreamingHelpers
streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
}
var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
var state = new StreamState(mediaSourceManager, transcodingJobType, transcodeManager)
{
Request = streamingRequest,
RequestedUrl = url,
@ -115,7 +115,7 @@ public static class StreamingHelpers
if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
{
var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)
? transcodeManager.GetTranscodingJob(streamingRequest.PlaySessionId)
: null;
if (currentJob is not null)

@ -41,6 +41,8 @@ public class IPBasedAccessValidationMiddleware
if (!networkManager.HasRemoteAccess(remoteIP))
{
// No access from network, respond with 503 instead of 200.
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
return;
}

@ -1,3 +1,4 @@
using System.Net;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@ -40,6 +41,8 @@ public class LanFilteringMiddleware
var host = httpContext.GetNormalizedRemoteIP();
if (!networkManager.IsInLocalNetwork(host))
{
// No access from network, respond with 503 instead of 200.
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
return;
}

@ -30,26 +30,11 @@ public class ClientCapabilitiesDto
/// </summary>
public bool SupportsMediaControl { get; set; }
/// <summary>
/// Gets or sets a value indicating whether session supports content uploading.
/// </summary>
public bool SupportsContentUploading { get; set; }
/// <summary>
/// Gets or sets the message callback url.
/// </summary>
public string? MessageCallbackUrl { get; set; }
/// <summary>
/// Gets or sets a value indicating whether session supports a persistent identifier.
/// </summary>
public bool SupportsPersistentIdentifier { get; set; }
/// <summary>
/// Gets or sets a value indicating whether session supports sync.
/// </summary>
public bool SupportsSync { get; set; }
/// <summary>
/// Gets or sets the device profile.
/// </summary>
@ -76,10 +61,7 @@ public class ClientCapabilitiesDto
PlayableMediaTypes = PlayableMediaTypes,
SupportedCommands = SupportedCommands,
SupportsMediaControl = SupportsMediaControl,
SupportsContentUploading = SupportsContentUploading,
MessageCallbackUrl = MessageCallbackUrl,
SupportsPersistentIdentifier = SupportsPersistentIdentifier,
SupportsSync = SupportsSync,
DeviceProfile = DeviceProfile,
AppStoreUrl = AppStoreUrl,
IconUrl = IconUrl

@ -1,4 +1,6 @@
namespace Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.Streaming;
namespace Jellyfin.Api.Models.StreamingDtos;
/// <summary>
/// The hls video request dto.

@ -1,4 +1,6 @@
namespace Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.Streaming;
namespace Jellyfin.Api.Models.StreamingDtos;
/// <summary>
/// The hls video request dto.

@ -0,0 +1,22 @@
namespace Jellyfin.Data.Enums;
/// <summary>
/// An enum representing formats of spatial audio.
/// </summary>
public enum AudioSpatialFormat
{
/// <summary>
/// None audio spatial format.
/// </summary>
None,
/// <summary>
/// Dolby Atmos audio spatial format.
/// </summary>
DolbyAtmos,
/// <summary>
/// DTS:X audio spatial format.
/// </summary>
DTSX,
}

@ -1,3 +1,4 @@
#pragma warning disable SA1300 // The name of a C# element does not begin with an upper-case letter. - disabled due to legacy requirement.
using Jellyfin.Data.Attributes;
namespace Jellyfin.Data.Enums;
@ -10,155 +11,155 @@ public enum CollectionType
/// <summary>
/// Unknown collection.
/// </summary>
Unknown = 0,
unknown = 0,
/// <summary>
/// Movies collection.
/// </summary>
Movies = 1,
movies = 1,
/// <summary>
/// Tv shows collection.
/// </summary>
TvShows = 2,
tvshows = 2,
/// <summary>
/// Music collection.
/// </summary>
Music = 3,
music = 3,
/// <summary>
/// Music videos collection.
/// </summary>
MusicVideos = 4,
musicvideos = 4,
/// <summary>
/// Trailers collection.
/// </summary>
Trailers = 5,
trailers = 5,
/// <summary>
/// Home videos collection.
/// </summary>
HomeVideos = 6,
homevideos = 6,
/// <summary>
/// Box sets collection.
/// </summary>
BoxSets = 7,
boxsets = 7,
/// <summary>
/// Books collection.
/// </summary>
Books = 8,
books = 8,
/// <summary>
/// Photos collection.
/// </summary>
Photos = 9,
photos = 9,
/// <summary>
/// Live tv collection.
/// </summary>
LiveTv = 10,
livetv = 10,
/// <summary>
/// Playlists collection.
/// </summary>
Playlists = 11,
playlists = 11,
/// <summary>
/// Folders collection.
/// </summary>
Folders = 12,
folders = 12,
/// <summary>
/// Tv show series collection.
/// </summary>
[OpenApiIgnoreEnum]
TvShowSeries = 101,
tvshowseries = 101,
/// <summary>
/// Tv genres collection.
/// </summary>
[OpenApiIgnoreEnum]
TvGenres = 102,
tvgenres = 102,
/// <summary>
/// Tv genre collection.
/// </summary>
[OpenApiIgnoreEnum]
TvGenre = 103,
tvgenre = 103,
/// <summary>
/// Tv latest collection.
/// </summary>
[OpenApiIgnoreEnum]
TvLatest = 104,
tvlatest = 104,
/// <summary>
/// Tv next up collection.
/// </summary>
[OpenApiIgnoreEnum]
TvNextUp = 105,
tvnextup = 105,
/// <summary>
/// Tv resume collection.
/// </summary>
[OpenApiIgnoreEnum]
TvResume = 106,
tvresume = 106,
/// <summary>
/// Tv favorite series collection.
/// </summary>
[OpenApiIgnoreEnum]
TvFavoriteSeries = 107,
tvfavoriteseries = 107,
/// <summary>
/// Tv favorite episodes collection.
/// </summary>
[OpenApiIgnoreEnum]
TvFavoriteEpisodes = 108,
tvfavoriteepisodes = 108,
/// <summary>
/// Latest movies collection.
/// </summary>
[OpenApiIgnoreEnum]
MovieLatest = 109,
movielatest = 109,
/// <summary>
/// Movies to resume collection.
/// </summary>
[OpenApiIgnoreEnum]
MovieResume = 110,
movieresume = 110,
/// <summary>
/// Movie movie collection.
/// </summary>
[OpenApiIgnoreEnum]
MovieMovies = 111,
moviemovies = 111,
/// <summary>
/// Movie collections collection.
/// </summary>
[OpenApiIgnoreEnum]
MovieCollections = 112,
moviecollection = 112,
/// <summary>
/// Movie favorites collection.
/// </summary>
[OpenApiIgnoreEnum]
MovieFavorites = 113,
moviefavorites = 113,
/// <summary>
/// Movie genres collection.
/// </summary>
[OpenApiIgnoreEnum]
MovieGenres = 114,
moviegenres = 114,
/// <summary>
/// Movie genre collection.
/// </summary>
[OpenApiIgnoreEnum]
MovieGenre = 115
moviegenre = 115
}

@ -110,21 +110,21 @@ namespace Jellyfin.Server.Implementations.Devices
/// <inheritdoc />
public async Task<DeviceInfo?> GetDevice(string id)
{
Device? device;
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
device = await dbContext.Devices
var device = await dbContext.Devices
.Where(d => d.DeviceId == id)
.OrderByDescending(d => d.DateLastActivity)
.Include(d => d.User)
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
.FirstOrDefaultAsync()
.ConfigureAwait(false);
}
var deviceInfo = device is null ? null : ToDeviceInfo(device);
var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
return deviceInfo;
return deviceInfo;
}
}
/// <inheritdoc />
@ -167,22 +167,18 @@ namespace Jellyfin.Server.Implementations.Devices
}
/// <inheritdoc />
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId)
{
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
IAsyncEnumerable<Device> sessions = dbContext.Devices
var sessions = dbContext.Devices
.Include(d => d.User)
.OrderByDescending(d => d.DateLastActivity)
.ThenBy(d => d.DeviceId)
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
.AsAsyncEnumerable();
if (supportsSync.HasValue)
{
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
}
if (userId.HasValue)
{
var user = _userManager.GetUserById(userId.Value);
@ -191,10 +187,10 @@ namespace Jellyfin.Server.Implementations.Devices
throw new ResourceNotFoundException();
}
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
}
var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false);
var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false);
return new QueryResult<DeviceInfo>(array);
}
@ -226,7 +222,7 @@ namespace Jellyfin.Server.Implementations.Devices
|| !GetCapabilities(deviceId).SupportsPersistentIdentifier;
}
private DeviceInfo ToDeviceInfo(Device authInfo)
private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
{
var caps = GetCapabilities(authInfo.DeviceId);
@ -239,7 +235,8 @@ namespace Jellyfin.Server.Implementations.Devices
LastUserName = authInfo.User.Username,
Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps.IconUrl
IconUrl = caps.IconUrl,
CustomName = options?.CustomName,
};
}
}

@ -1,7 +1,7 @@
#pragma warning disable CA1307
#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@ -46,8 +46,6 @@ namespace Jellyfin.Server.Implementations.Users
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary<Guid, User> _users;
/// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary>
@ -58,6 +56,8 @@ namespace Jellyfin.Server.Implementations.Users
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
/// <param name="serverConfigurationManager">The system config manager.</param>
/// <param name="passwordResetProviders">The password reset providers.</param>
/// <param name="authenticationProviders">The authentication providers.</param>
public UserManager(
IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager,
@ -65,7 +65,9 @@ namespace Jellyfin.Server.Implementations.Users
IApplicationHost appHost,
IImageProcessor imageProcessor,
ILogger<UserManager> logger,
IServerConfigurationManager serverConfigurationManager)
IServerConfigurationManager serverConfigurationManager,
IEnumerable<IPasswordResetProvider> passwordResetProviders,
IEnumerable<IAuthenticationProvider> authenticationProviders)
{
_dbProvider = dbProvider;
_eventManager = eventManager;
@ -75,35 +77,36 @@ namespace Jellyfin.Server.Implementations.Users
_logger = logger;
_serverConfigurationManager = serverConfigurationManager;
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
_passwordResetProviders = passwordResetProviders.ToList();
_authenticationProviders = authenticationProviders.ToList();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
_users = new ConcurrentDictionary<Guid, User>();
using var dbContext = _dbProvider.CreateDbContext();
foreach (var user in dbContext.Users
.AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.AsEnumerable())
{
_users.Add(user.Id, user);
}
}
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
/// <inheritdoc/>
public IEnumerable<User> Users => _users.Values;
public IEnumerable<User> Users
{
get
{
using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext).ToList();
}
}
/// <inheritdoc/>
public IEnumerable<Guid> UsersIds => _users.Keys;
public IEnumerable<Guid> UsersIds
{
get
{
using var dbContext = _dbProvider.CreateDbContext();
return dbContext.Users.Select(u => u.Id).ToList();
}
}
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
@ -119,8 +122,8 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Guid can't be empty", nameof(id));
}
_users.TryGetValue(id, out var user);
return user;
using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id));
}
/// <inheritdoc/>
@ -131,7 +134,9 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(name));
}
return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext)
.FirstOrDefault(u => string.Equals(u.Username, name));
}
/// <inheritdoc/>
@ -196,8 +201,6 @@ namespace Jellyfin.Server.Implementations.Users
user.AddDefaultPermissions();
user.AddDefaultPreferences();
_users.Add(user.Id, user);
return user;
}
@ -232,40 +235,46 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public async Task DeleteUserAsync(Guid userId)
{
if (!_users.TryGetValue(userId, out var user))
{
throw new ResourceNotFoundException(nameof(userId));
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
if (_users.Count == 1)
await using (dbContext.ConfigureAwait(false))
{
throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one user in the system.",
user.Username));
}
var user = await dbContext.Users
.AsSingleQuery()
.Include(u => u.Permissions)
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
.ConfigureAwait(false);
if (user is null)
{
throw new ResourceNotFoundException(nameof(userId));
}
if (user.HasPermission(PermissionKind.IsAdministrator)
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{
throw new ArgumentException(
string.Format(
if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1)
{
throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
user.Username),
nameof(userId));
}
"The user '{0}' cannot be deleted because there must be at least one user in the system.",
user.Username));
}
if (user.HasPermission(PermissionKind.IsAdministrator)
&& await dbContext.Users
.CountAsync(u => u.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value))
.ConfigureAwait(false) == 1)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
user.Username),
nameof(userId));
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
_users.Remove(userId);
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
}
}
/// <inheritdoc/>
@ -532,23 +541,23 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc />
public async Task InitializeAsync()
{
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
if (_users.Any())
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
return;
}
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
{
return;
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
{
defaultName = "MyJellyfinUser";
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
{
defaultName = "MyJellyfinUser";
}
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
@ -595,12 +604,9 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
var user = await GetUsersInternal(dbContext)
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
.ConfigureAwait(false)
?? throw new ArgumentException("No user exists with given Id!");
user.SubtitleMode = config.SubtitleMode;
@ -628,7 +634,6 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
@ -639,12 +644,9 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
var user = await GetUsersInternal(dbContext)
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
.ConfigureAwait(false)
?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
@ -704,7 +706,6 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
@ -725,7 +726,6 @@ namespace Jellyfin.Server.Implementations.Users
}
user.ProfileImage = null;
_users[user.Id] = user;
}
internal static void ThrowIfInvalidUsername(string name)
@ -872,8 +872,15 @@ namespace Jellyfin.Server.Implementations.Users
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{
dbContext.Users.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
private IQueryable<User> GetUsersInternal(JellyfinDbContext dbContext)
=> dbContext.Users
.AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage);
}
}

@ -14,6 +14,7 @@ using Jellyfin.Server.Implementations.Security;
using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
@ -78,6 +79,9 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IUserManager, UserManager>();
serviceCollection.AddSingleton<IAuthenticationProvider, DefaultAuthenticationProvider>();
serviceCollection.AddSingleton<IAuthenticationProvider, InvalidAuthProvider>();
serviceCollection.AddSingleton<IPasswordResetProvider, DefaultPasswordResetProvider>();
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();

@ -6,6 +6,7 @@ using System.Net.Mime;
using System.Text;
using Jellyfin.Api.Middleware;
using Jellyfin.MediaEncoding.Hls.Extensions;
using Jellyfin.Networking;
using Jellyfin.Networking.HappyEyeballs;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.HealthChecks;
@ -13,7 +14,6 @@ using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Extensions;
using Jellyfin.Server.Infrastructure;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Builder;
@ -121,6 +121,8 @@ namespace Jellyfin.Server
.AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
services.AddHlsPlaylistGenerator();
services.AddHostedService<AutoDiscoveryHost>();
}
/// <summary>

@ -59,9 +59,8 @@ namespace MediaBrowser.Controller.Devices
/// Gets the devices.
/// </summary>
/// <param name="userId">The user's id, or <c>null</c>.</param>
/// <param name="supportsSync">A value indicating whether the device supports sync, or <c>null</c>.</param>
/// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync);
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
Task DeleteDevice(Device device);

@ -724,7 +724,7 @@ namespace MediaBrowser.Controller.Entities
if (this is IHasCollectionType view)
{
if (view.CollectionType == CollectionType.LiveTv)
if (view.CollectionType == CollectionType.livetv)
{
return true;
}
@ -773,8 +773,6 @@ namespace MediaBrowser.Controller.Entities
/// <value>The remote trailers.</value>
public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
public virtual bool SupportsExternalTransfer => false;
public virtual double GetDefaultPrimaryImageAspectRatio()
{
return 0;

@ -19,19 +19,19 @@ namespace MediaBrowser.Controller.Entities
{
private static readonly CollectionType?[] _viewTypesEligibleForGrouping =
{
Jellyfin.Data.Enums.CollectionType.Movies,
Jellyfin.Data.Enums.CollectionType.TvShows,
Jellyfin.Data.Enums.CollectionType.movies,
Jellyfin.Data.Enums.CollectionType.tvshows,
null
};
private static readonly CollectionType?[] _originalFolderViewTypes =
{
Jellyfin.Data.Enums.CollectionType.Books,
Jellyfin.Data.Enums.CollectionType.MusicVideos,
Jellyfin.Data.Enums.CollectionType.HomeVideos,
Jellyfin.Data.Enums.CollectionType.Photos,
Jellyfin.Data.Enums.CollectionType.Music,
Jellyfin.Data.Enums.CollectionType.BoxSets
Jellyfin.Data.Enums.CollectionType.books,
Jellyfin.Data.Enums.CollectionType.musicvideos,
Jellyfin.Data.Enums.CollectionType.homevideos,
Jellyfin.Data.Enums.CollectionType.photos,
Jellyfin.Data.Enums.CollectionType.music,
Jellyfin.Data.Enums.CollectionType.boxsets
};
public static ITVSeriesManager TVSeriesManager { get; set; }
@ -161,7 +161,7 @@ namespace MediaBrowser.Controller.Entities
return true;
}
return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.Playlists;
return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.playlists;
}
public static bool IsEligibleForGrouping(Folder folder)

@ -58,58 +58,58 @@ namespace MediaBrowser.Controller.Entities
switch (viewType)
{
case CollectionType.Folders:
case CollectionType.folders:
return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query);
case CollectionType.TvShows:
case CollectionType.tvshows:
return GetTvView(queryParent, user, query);
case CollectionType.Movies:
case CollectionType.movies:
return GetMovieFolders(queryParent, user, query);
case CollectionType.TvShowSeries:
case CollectionType.tvshowseries:
return GetTvSeries(queryParent, user, query);
case CollectionType.TvGenres:
case CollectionType.tvgenres:
return GetTvGenres(queryParent, user, query);
case CollectionType.TvGenre:
case CollectionType.tvgenre:
return GetTvGenreItems(queryParent, displayParent, user, query);
case CollectionType.TvResume:
case CollectionType.tvresume:
return GetTvResume(queryParent, user, query);
case CollectionType.TvNextUp:
case CollectionType.tvnextup:
return GetTvNextUp(queryParent, query);
case CollectionType.TvLatest:
case CollectionType.tvlatest:
return GetTvLatest(queryParent, user, query);
case CollectionType.MovieFavorites:
case CollectionType.moviefavorites:
return GetFavoriteMovies(queryParent, user, query);
case CollectionType.MovieLatest:
case CollectionType.movielatest:
return GetMovieLatest(queryParent, user, query);
case CollectionType.MovieGenres:
case CollectionType.moviegenres:
return GetMovieGenres(queryParent, user, query);
case CollectionType.MovieGenre:
case CollectionType.moviegenre:
return GetMovieGenreItems(queryParent, displayParent, user, query);
case CollectionType.MovieResume:
case CollectionType.movieresume:
return GetMovieResume(queryParent, user, query);
case CollectionType.MovieMovies:
case CollectionType.moviemovies:
return GetMovieMovies(queryParent, user, query);
case CollectionType.MovieCollections:
case CollectionType.moviecollection:
return GetMovieCollections(user, query);
case CollectionType.TvFavoriteEpisodes:
case CollectionType.tvfavoriteepisodes:
return GetFavoriteEpisodes(queryParent, user, query);
case CollectionType.TvFavoriteSeries:
case CollectionType.tvfavoriteseries:
return GetFavoriteSeries(queryParent, user, query);
default:
@ -146,12 +146,12 @@ namespace MediaBrowser.Controller.Entities
var list = new List<BaseItem>
{
GetUserView(CollectionType.MovieResume, "HeaderContinueWatching", "0", parent),
GetUserView(CollectionType.MovieLatest, "Latest", "1", parent),
GetUserView(CollectionType.MovieMovies, "Movies", "2", parent),
GetUserView(CollectionType.MovieCollections, "Collections", "3", parent),
GetUserView(CollectionType.MovieFavorites, "Favorites", "4", parent),
GetUserView(CollectionType.MovieGenres, "Genres", "5", parent)
GetUserView(CollectionType.movieresume, "HeaderContinueWatching", "0", parent),
GetUserView(CollectionType.movielatest, "Latest", "1", parent),
GetUserView(CollectionType.moviemovies, "Movies", "2", parent),
GetUserView(CollectionType.moviecollection, "Collections", "3", parent),
GetUserView(CollectionType.moviefavorites, "Favorites", "4", parent),
GetUserView(CollectionType.moviegenres, "Genres", "5", parent)
};
return GetResult(list, query);
@ -264,7 +264,7 @@ namespace MediaBrowser.Controller.Entities
}
})
.Where(i => i is not null)
.Select(i => GetUserViewWithName(CollectionType.MovieGenre, i.SortName, parent));
.Select(i => GetUserViewWithName(CollectionType.moviegenre, i.SortName, parent));
return GetResult(genres, query);
}
@ -303,13 +303,13 @@ namespace MediaBrowser.Controller.Entities
var list = new List<BaseItem>
{
GetUserView(CollectionType.TvResume, "HeaderContinueWatching", "0", parent),
GetUserView(CollectionType.TvNextUp, "HeaderNextUp", "1", parent),
GetUserView(CollectionType.TvLatest, "Latest", "2", parent),
GetUserView(CollectionType.TvShowSeries, "Shows", "3", parent),
GetUserView(CollectionType.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent),
GetUserView(CollectionType.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent),
GetUserView(CollectionType.TvGenres, "Genres", "6", parent)
GetUserView(CollectionType.tvresume, "HeaderContinueWatching", "0", parent),
GetUserView(CollectionType.tvnextup, "HeaderNextUp", "1", parent),
GetUserView(CollectionType.tvlatest, "Latest", "2", parent),
GetUserView(CollectionType.tvshowseries, "Shows", "3", parent),
GetUserView(CollectionType.tvfavoriteseries, "HeaderFavoriteShows", "4", parent),
GetUserView(CollectionType.tvfavoriteepisodes, "HeaderFavoriteEpisodes", "5", parent),
GetUserView(CollectionType.tvgenres, "Genres", "6", parent)
};
return GetResult(list, query);
@ -330,7 +330,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query)
{
var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows });
var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.tvshows });
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
@ -392,7 +392,7 @@ namespace MediaBrowser.Controller.Entities
}
})
.Where(i => i is not null)
.Select(i => GetUserViewWithName(CollectionType.TvGenre, i.SortName, parent));
.Select(i => GetUserViewWithName(CollectionType.tvgenre, i.SortName, parent));
return GetResult(genres, query);
}

@ -2,6 +2,7 @@
#pragma warning disable CA1711, CS1591
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -9,7 +10,7 @@ using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.Library
{
public interface ILiveStream
public interface ILiveStream : IDisposable
{
int ConsumerCount { get; set; }

@ -35,6 +35,15 @@ namespace MediaBrowser.Controller.Library
void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
/// <summary>
/// Save the provided user data for the given user.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="item">The item.</param>
/// <param name="userDataDto">The reason for updating the user data.</param>
/// <param name="reason">The reason.</param>
void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason);
UserItemData GetUserData(User user, BaseItem item);
UserItemData GetUserData(Guid userId, BaseItem item);

@ -36,7 +36,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <value>The services.</value>
IReadOnlyList<ILiveTvService> Services { get; }
IListingsProvider[] ListingProviders { get; }
IReadOnlyList<IListingsProvider> ListingProviders { get; }
/// <summary>
/// Gets the new timer defaults asynchronous.

@ -50,7 +50,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="currentLiveStreams">The current live streams.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>Live stream wrapped in a task.</returns>
Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
Task<ILiveStream> GetChannelStream(string channelId, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
/// <summary>
/// Gets the channel stream media sources.

@ -1068,7 +1068,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// hw transpose filters should be added manually.
args.Append(" -autorotate 0");
args.Append(" -noautorotate");
return args.ToString().Trim();
}
@ -1159,7 +1159,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options));
if (!isSwDecoder && _mediaEncoder.EncoderVersion >= new Version(4, 4))
{
arg.Append(" -autoscale 0");
arg.Append(" -noautoscale");
}
return arg.ToString();
@ -3343,7 +3343,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// [0:s]scale=s=1280x720
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
return (mainFilters, subFilters, overlayFilters);
@ -3520,7 +3520,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
subFilters.Add("hwupload=derive_device=cuda");
overlayFilters.Add("overlay_cuda=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("overlay_cuda=eof_action=pass:repeatlast=0");
}
}
else
@ -3529,7 +3529,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@ -3718,7 +3718,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
subFilters.Add("hwupload=derive_device=opencl");
overlayFilters.Add("overlay_opencl=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0");
overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1");
overlayFilters.Add("format=d3d11");
}
@ -3729,7 +3729,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@ -3964,7 +3964,7 @@ namespace MediaBrowser.Controller.MediaEncoding
: string.Empty;
var overlayQsvFilter = string.Format(
CultureInfo.InvariantCulture,
"overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}",
"overlay_qsv=eof_action=pass:repeatlast=0{0}",
overlaySize);
overlayFilters.Add(overlayQsvFilter);
}
@ -3975,7 +3975,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@ -4180,7 +4180,7 @@ namespace MediaBrowser.Controller.MediaEncoding
: string.Empty;
var overlayQsvFilter = string.Format(
CultureInfo.InvariantCulture,
"overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}",
"overlay_qsv=eof_action=pass:repeatlast=0{0}",
overlaySize);
overlayFilters.Add(overlayQsvFilter);
}
@ -4191,7 +4191,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@ -4445,7 +4445,7 @@ namespace MediaBrowser.Controller.MediaEncoding
: string.Empty;
var overlayVaapiFilter = string.Format(
CultureInfo.InvariantCulture,
"overlay_vaapi=eof_action=endall:shortest=1:repeatlast=0{0}",
"overlay_vaapi=eof_action=pass:repeatlast=0{0}",
overlaySize);
overlayFilters.Add(overlayVaapiFilter);
}
@ -4456,7 +4456,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
if (isVaapiEncoder)
{
@ -4616,7 +4616,7 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add("hwupload=derive_device=vulkan");
subFilters.Add("format=vulkan");
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("overlay_vulkan=eof_action=pass:repeatlast=0");
if (isSwEncoder)
{
@ -4817,7 +4817,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
if (isVaapiEncoder)
{

@ -0,0 +1,104 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Streaming;
namespace MediaBrowser.Controller.MediaEncoding;
/// <summary>
/// A service for managing media transcoding.
/// </summary>
public interface ITranscodeManager
{
/// <summary>
/// Get transcoding job.
/// </summary>
/// <param name="playSessionId">Playback session id.</param>
/// <returns>The transcoding job.</returns>
public TranscodingJob? GetTranscodingJob(string playSessionId);
/// <summary>
/// Get transcoding job.
/// </summary>
/// <param name="path">Path to the transcoding file.</param>
/// <param name="type">The <see cref="TranscodingJobType"/>.</param>
/// <returns>The transcoding job.</returns>
public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type);
/// <summary>
/// Ping transcoding job.
/// </summary>
/// <param name="playSessionId">Play session id.</param>
/// <param name="isUserPaused">Is user paused.</param>
/// <exception cref="ArgumentNullException">Play session id is null.</exception>
public void PingTranscodingJob(string playSessionId, bool? isUserPaused);
/// <summary>
/// Kills the single transcoding job.
/// </summary>
/// <param name="deviceId">The device id.</param>
/// <param name="playSessionId">The play session identifier.</param>
/// <param name="deleteFiles">The delete files.</param>
/// <returns>Task.</returns>
public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles);
/// <summary>
/// Report the transcoding progress to the session manager.
/// </summary>
/// <param name="job">The <see cref="TranscodingJob"/> of which the progress will be reported.</param>
/// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param>
/// <param name="transcodingPosition">The current transcoding position.</param>
/// <param name="framerate">The framerate of the transcoding job.</param>
/// <param name="percentComplete">The completion percentage of the transcode.</param>
/// <param name="bytesTranscoded">The number of bytes transcoded.</param>
/// <param name="bitRate">The bitrate of the transcoding job.</param>
public void ReportTranscodingProgress(
TranscodingJob job,
StreamState state,
TimeSpan? transcodingPosition,
float? framerate,
double? percentComplete,
long? bytesTranscoded,
int? bitRate);
/// <summary>
/// Starts FFMpeg.
/// </summary>
/// <param name="state">The state.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="commandLineArguments">The command line arguments for FFmpeg.</param>
/// <param name="userId">The user id.</param>
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationTokenSource">The cancellation token source.</param>
/// <param name="workingDirectory">The working directory.</param>
/// <returns>Task.</returns>
public Task<TranscodingJob> StartFfMpeg(
StreamState state,
string outputPath,
string commandLineArguments,
Guid userId,
TranscodingJobType transcodingJobType,
CancellationTokenSource cancellationTokenSource,
string? workingDirectory = null);
/// <summary>
/// Called when [transcode begin request].
/// </summary>
/// <param name="path">The path.</param>
/// <param name="type">The type.</param>
/// <returns>The <see cref="TranscodingJob"/>.</returns>
public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type);
/// <summary>
/// Called when [transcode end].
/// </summary>
/// <param name="job">The transcode job.</param>
public void OnTranscodeEndRequest(TranscodingJob job);
/// <summary>
/// Gets the transcoding lock.
/// </summary>
/// <param name="outputPath">The output path of the transcoded file.</param>
/// <returns>A <see cref="SemaphoreSlim"/>.</returns>
public SemaphoreSlim GetTranscodingLock(string outputPath);
}

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

Loading…
Cancel
Save