mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-04-02 08:21:56 +01:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2aa80ce5c | ||
|
|
ff365dae34 | ||
|
|
52aebfb7d3 | ||
|
|
66ea1b50e6 | ||
|
|
3f656ade7a | ||
|
|
8bf0d372c6 | ||
|
|
202d7b5829 | ||
|
|
352e4f3aba | ||
|
|
c5f6d00c94 | ||
|
|
e8d1d94436 | ||
|
|
50dc37065b | ||
|
|
7e88b18192 | ||
|
|
89e914c7f1 | ||
|
|
1932ac4765 | ||
|
|
ec33c74ec4 | ||
|
|
2184ed1b16 | ||
|
|
d3907afde7 | ||
|
|
e12d933531 | ||
|
|
c0ba29d917 | ||
|
|
d1fd81c382 | ||
|
|
e038045494 | ||
|
|
e1691e649e | ||
|
|
8d28497d29 | ||
|
|
fddd4e7e6b | ||
|
|
0581cd6610 | ||
|
|
0f1732e5f5 | ||
|
|
41c2d51d8c | ||
|
|
29b2361857 | ||
|
|
ce867f9834 | ||
|
|
4034bf9d7e | ||
|
|
3d2658fa43 | ||
|
|
61b19688ff | ||
|
|
e8d72bf6a3 | ||
|
|
348b14f7b7 | ||
|
|
fda49a5a49 | ||
|
|
55c00d76bb | ||
|
|
519d2113eb | ||
|
|
f34f6b6941 | ||
|
|
6864e108b8 | ||
|
|
09ba04662a | ||
|
|
9cd2418095 | ||
|
|
b6a96513de | ||
|
|
ca57166e95 | ||
|
|
33496c1693 | ||
|
|
b65daeca0b | ||
|
|
286cc6d720 | ||
|
|
aa4f09c799 | ||
|
|
afd3c0d9f3 | ||
|
|
5597d8e1a7 | ||
|
|
0166362258 | ||
|
|
58c330b63d | ||
|
|
be71295693 | ||
|
|
8cd3090cee | ||
|
|
7bf08daeec | ||
|
|
290463fe7b | ||
|
|
1b2d9c100a | ||
|
|
caa05c1bf2 | ||
|
|
a37ead86df | ||
|
|
e65aff8bc6 | ||
|
|
9734494eb6 | ||
|
|
d41e302418 | ||
|
|
80ba517294 | ||
|
|
95d08b264f | ||
|
|
893a849f28 | ||
|
|
673f617994 | ||
|
|
644327eb76 |
4
.github/workflows/ci-compat.yml
vendored
4
.github/workflows/ci-compat.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: ABI Compatibility
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
||||
|
||||
name: ABI - Difference
|
||||
if: ${{ github.event_name == 'pull_request_target' }}
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- abi-head
|
||||
|
||||
6
.github/workflows/ci-openapi.yml
vendored
6
.github/workflows/ci-openapi.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
||||
|
||||
name: OpenAPI - Difference
|
||||
if: ${{ github.event_name == 'pull_request_target' }}
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- openapi-head
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
|
||||
publish-unstable:
|
||||
name: OpenAPI - Publish Unstable Spec
|
||||
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- openapi-head
|
||||
|
||||
2
.github/workflows/commands.yml
vendored
2
.github/workflows/commands.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
types:
|
||||
- created
|
||||
- edited
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- synchronize
|
||||
|
||||
2
.github/workflows/project-automation.yml
vendored
2
.github/workflows/project-automation.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
issue_comment:
|
||||
|
||||
permissions: {}
|
||||
|
||||
4
.github/workflows/pull-request-conflict.yml
vendored
4
.github/workflows/pull-request-conflict.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
issue_comment:
|
||||
|
||||
permissions: {}
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request'}}
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<VersionPrefix>10.11.7</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -267,22 +267,24 @@ namespace Emby.Server.Implementations.Images
|
||||
{
|
||||
var image = item.GetImageInfo(type, 0);
|
||||
|
||||
if (image is not null)
|
||||
if (image is null)
|
||||
{
|
||||
if (!image.IsLocalFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return GetItemsWithImages(item).Count is not 0;
|
||||
}
|
||||
|
||||
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!image.IsLocalFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!HasChangedByDate(item, image))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!HasChangedByDate(item, image))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -50,6 +50,10 @@ namespace Emby.Server.Implementations.Library
|
||||
"**/lost+found",
|
||||
"**/subs/**",
|
||||
"**/subs",
|
||||
"**/.snapshots/**",
|
||||
"**/.snapshots",
|
||||
"**/.snapshot/**",
|
||||
"**/.snapshot",
|
||||
|
||||
// Trickplay files
|
||||
"**/*.trickplay",
|
||||
@@ -83,7 +87,6 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
// Unix hidden files
|
||||
"**/.*",
|
||||
"**/.*/**",
|
||||
|
||||
// Mac - if you ever remove the above.
|
||||
// "**/._*",
|
||||
|
||||
@@ -1175,7 +1175,8 @@ namespace Emby.Server.Implementations.Session
|
||||
return session;
|
||||
}
|
||||
|
||||
private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
|
||||
/// <inheritdoc />
|
||||
public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
|
||||
{
|
||||
return new SessionInfoDto
|
||||
{
|
||||
|
||||
@@ -92,18 +92,18 @@ public class AudioController : BaseJellyfinApiController
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetAudioStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -114,7 +114,7 @@ public class AudioController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -133,8 +133,8 @@ public class AudioController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -259,18 +259,18 @@ public class AudioController : BaseJellyfinApiController
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetAudioStreamByContainer(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -281,7 +281,7 @@ public class AudioController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -300,8 +300,8 @@ public class AudioController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
|
||||
@@ -167,18 +167,18 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[ProducesPlaylistFile]
|
||||
public async Task<ActionResult> GetLiveHlsStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -189,7 +189,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -208,8 +208,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -416,12 +416,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery, Required] string mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -432,7 +432,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -453,8 +453,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -592,12 +592,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery, Required] string mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -609,7 +609,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -628,8 +628,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -762,12 +762,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -778,7 +778,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -799,8 +799,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -934,12 +934,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -951,7 +951,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -970,8 +970,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1107,7 +1107,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromRoute, Required] int segmentId,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
||||
[FromQuery, Required] long runtimeTicks,
|
||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||
[FromQuery] bool? @static,
|
||||
@@ -1115,12 +1115,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -1131,7 +1131,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -1152,8 +1152,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1292,7 +1292,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromRoute, Required] int segmentId,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
||||
[FromQuery, Required] long runtimeTicks,
|
||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||
[FromQuery] bool? @static,
|
||||
@@ -1300,12 +1300,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -1317,7 +1317,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -1336,8 +1336,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1421,10 +1421,20 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
var mediaSourceId = state.BaseRequest.MediaSourceId;
|
||||
double fps = state.TargetFramerate ?? 0.0f;
|
||||
int segmentLength = state.SegmentLength * 1000;
|
||||
|
||||
// If video is transcoded and framerate is fractional (i.e. 23.976), we need to slightly adjust segment length
|
||||
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001)
|
||||
{
|
||||
double nearestIntFramerate = Math.Ceiling(fps);
|
||||
segmentLength = (int)Math.Ceiling(segmentLength * (nearestIntFramerate / fps));
|
||||
}
|
||||
|
||||
var request = new CreateMainPlaylistRequest(
|
||||
mediaSourceId is null ? null : Guid.Parse(mediaSourceId),
|
||||
state.MediaPath,
|
||||
state.SegmentLength * 1000,
|
||||
segmentLength,
|
||||
state.RunTimeTicks ?? 0,
|
||||
state.Request.SegmentContainer ?? string.Empty,
|
||||
"hls1/main/",
|
||||
|
||||
@@ -458,7 +458,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Tuners/{tunerId}/Reset")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
|
||||
{
|
||||
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -983,7 +983,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Created tuner host returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
|
||||
[HttpPost("TunerHosts")]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
|
||||
=> await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
|
||||
@@ -995,7 +995,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="204">Tuner host deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("TunerHosts")]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteTunerHost([FromQuery] string? id)
|
||||
{
|
||||
@@ -1028,7 +1028,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Created listings provider returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
||||
[HttpPost("ListingProviders")]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
|
||||
@@ -1054,7 +1054,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="204">Listing provider deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("ListingProviders")]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteListingProvider([FromQuery] string? id)
|
||||
{
|
||||
@@ -1087,7 +1087,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Available countries returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
|
||||
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||
public async Task<ActionResult> GetSchedulesDirectCountries()
|
||||
@@ -1108,7 +1108,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Channel mapping options returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
|
||||
[HttpGet("ChannelMappingOptions")]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId)
|
||||
=> _listingsManager.GetChannelMappingOptions(providerId);
|
||||
@@ -1120,7 +1120,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <response code="200">Created channel mapping returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
||||
[HttpPost("ChannelMappings")]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto)
|
||||
=> _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId);
|
||||
@@ -1144,7 +1144,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
|
||||
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
|
||||
[HttpGet("Tuners/Discover")]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
|
||||
=> _tunerHostManager.DiscoverTuners(newDevicesOnly);
|
||||
@@ -1192,7 +1192,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
[ProducesVideoFile]
|
||||
public ActionResult GetLiveStreamFile(
|
||||
[FromRoute, Required] string streamId,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container)
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container)
|
||||
{
|
||||
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
||||
if (liveStreamInfo is null)
|
||||
|
||||
@@ -58,7 +58,7 @@ public class SyncPlayController : BaseJellyfinApiController
|
||||
[FromBody, Required] NewGroupRequestDto requestData)
|
||||
{
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
|
||||
var syncPlayRequest = new NewGroupRequest(requestData.GroupName.Trim());
|
||||
return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None));
|
||||
}
|
||||
|
||||
|
||||
@@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] int? transcodingAudioChannels,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer,
|
||||
[FromQuery] MediaStreamProtocol? transcodingProtocol,
|
||||
[FromQuery] int? maxAudioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
|
||||
@@ -315,18 +315,18 @@ public class VideosController : BaseJellyfinApiController
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetVideoStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -337,7 +337,7 @@ public class VideosController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -358,8 +358,8 @@ public class VideosController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -556,18 +556,18 @@ public class VideosController : BaseJellyfinApiController
|
||||
[ProducesVideoFile]
|
||||
public Task<ActionResult> GetVideoStreamByContainer(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -578,7 +578,7 @@ public class VideosController : BaseJellyfinApiController
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
@@ -599,8 +599,8 @@ public class VideosController : BaseJellyfinApiController
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Extensions;
|
||||
@@ -17,9 +18,7 @@ using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Streaming;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Jellyfin.Api.Helpers;
|
||||
@@ -422,14 +421,18 @@ public static class StreamingHelpers
|
||||
request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
case 4:
|
||||
if (videoRequest is not null)
|
||||
if (videoRequest is not null && IsValidCodecName(val))
|
||||
{
|
||||
videoRequest.VideoCodec = val;
|
||||
}
|
||||
|
||||
break;
|
||||
case 5:
|
||||
request.AudioCodec = val;
|
||||
if (IsValidCodecName(val))
|
||||
{
|
||||
request.AudioCodec = val;
|
||||
}
|
||||
|
||||
break;
|
||||
case 6:
|
||||
if (videoRequest is not null)
|
||||
@@ -483,7 +486,7 @@ public static class StreamingHelpers
|
||||
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 15:
|
||||
if (videoRequest is not null)
|
||||
if (videoRequest is not null && Regex.IsMatch(val, EncodingHelper.LevelValidationRegexStr))
|
||||
{
|
||||
videoRequest.Level = val;
|
||||
}
|
||||
@@ -504,7 +507,7 @@ public static class StreamingHelpers
|
||||
|
||||
break;
|
||||
case 18:
|
||||
if (videoRequest is not null)
|
||||
if (videoRequest is not null && IsValidCodecName(val))
|
||||
{
|
||||
videoRequest.Profile = val;
|
||||
}
|
||||
@@ -563,7 +566,11 @@ public static class StreamingHelpers
|
||||
|
||||
break;
|
||||
case 30:
|
||||
request.SubtitleCodec = val;
|
||||
if (IsValidCodecName(val))
|
||||
{
|
||||
request.SubtitleCodec = val;
|
||||
}
|
||||
|
||||
break;
|
||||
case 31:
|
||||
if (videoRequest is not null)
|
||||
@@ -586,6 +593,11 @@ public static class StreamingHelpers
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidCodecName(string val)
|
||||
{
|
||||
return EncodingHelper.ContainerValidationRegex().IsMatch(val);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the container into its file extension.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Jellyfin.Api.Models.SyncPlayDtos;
|
||||
|
||||
/// <summary>
|
||||
@@ -17,5 +19,6 @@ public class NewGroupRequestDto
|
||||
/// Gets or sets the group name.
|
||||
/// </summary>
|
||||
/// <value>The name of the new group.</value>
|
||||
[StringLength(200, ErrorMessage = "Group name must not exceed 200 characters.")]
|
||||
public string GroupName { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -15,7 +16,7 @@ namespace Jellyfin.Api.WebSocketListeners;
|
||||
/// <summary>
|
||||
/// Class SessionInfoWebSocketListener.
|
||||
/// </summary>
|
||||
public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
|
||||
public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfoDto>, WebSocketListenerState>
|
||||
{
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private bool _disposed;
|
||||
@@ -52,24 +53,26 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
||||
/// Gets the data to send.
|
||||
/// </summary>
|
||||
/// <returns>Task{SystemInfo}.</returns>
|
||||
protected override Task<IEnumerable<SessionInfo>> GetDataToSend()
|
||||
protected override Task<IEnumerable<SessionInfoDto>> GetDataToSend()
|
||||
{
|
||||
return Task.FromResult(_sessionManager.Sessions);
|
||||
return Task.FromResult(_sessionManager.Sessions.Select(_sessionManager.ToSessionInfoDto));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<IEnumerable<SessionInfo>> GetDataToSendForConnection(IWebSocketConnection connection)
|
||||
protected override Task<IEnumerable<SessionInfoDto>> GetDataToSendForConnection(IWebSocketConnection connection)
|
||||
{
|
||||
var sessions = _sessionManager.Sessions;
|
||||
|
||||
// For non-admin users, filter the sessions to only include their own sessions
|
||||
if (connection.AuthorizationInfo?.User is not null &&
|
||||
!connection.AuthorizationInfo.IsApiKey &&
|
||||
!connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
||||
{
|
||||
var userId = connection.AuthorizationInfo.User.Id;
|
||||
return Task.FromResult(_sessionManager.Sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId)));
|
||||
sessions = sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId));
|
||||
}
|
||||
|
||||
return Task.FromResult(_sessionManager.Sessions);
|
||||
return Task.FromResult(sessions.Select(_sessionManager.ToSessionInfoDto));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<VersionPrefix>10.11.7</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -185,7 +185,7 @@ public static class UserEntityExtensions
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, false));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
|
||||
entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
|
||||
|
||||
@@ -118,15 +118,21 @@ public class BackupService : IBackupService
|
||||
throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
|
||||
}
|
||||
|
||||
void CopyDirectory(string source, string target)
|
||||
void CopyDirectory(string source, string target, string[]? exclude = null)
|
||||
{
|
||||
var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
|
||||
var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
|
||||
var excludePaths = exclude?.Select(e => $"{source}/{e}/").ToArray();
|
||||
foreach (var item in zipArchive.Entries)
|
||||
{
|
||||
var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
|
||||
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
|
||||
|
||||
if (excludePaths is not null && excludePaths.Any(e => item.FullName.StartsWith(e, StringComparison.Ordinal)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|
||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|
||||
|| Path.EndsInDirectorySeparator(item.FullName))
|
||||
@@ -142,8 +148,10 @@ public class BackupService : IBackupService
|
||||
}
|
||||
|
||||
CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
|
||||
CopyDirectory("Data", _applicationPaths.DataPath);
|
||||
CopyDirectory("Data", _applicationPaths.DataPath, exclude: ["metadata", "metadata-default"]);
|
||||
CopyDirectory("Root", _applicationPaths.RootFolderPath);
|
||||
CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath);
|
||||
CopyDirectory("Data/metadata-default", _applicationPaths.DefaultInternalMetadataPath);
|
||||
|
||||
if (manifest.Options.Database)
|
||||
{
|
||||
@@ -403,6 +411,15 @@ public class BackupService : IBackupService
|
||||
if (backupOptions.Metadata)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||
|
||||
// If a custom metadata path is configured, the default location may still contain data.
|
||||
if (!string.Equals(
|
||||
Path.GetFullPath(_applicationPaths.DefaultInternalMetadataPath),
|
||||
Path.GetFullPath(_applicationPaths.InternalMetadataPath),
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DefaultInternalMetadataPath), Path.Combine("Data", "metadata-default"));
|
||||
}
|
||||
}
|
||||
|
||||
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
||||
|
||||
@@ -295,6 +295,25 @@ public sealed class BaseItemRepository
|
||||
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
|
||||
var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
|
||||
if (hasRandomSort)
|
||||
{
|
||||
var orderedIds = dbQuery.Select(e => e.Id).ToList();
|
||||
if (orderedIds.Count == 0)
|
||||
{
|
||||
return Array.Empty<BaseItemDto>();
|
||||
}
|
||||
|
||||
var itemsById = ApplyNavigations(context.BaseItems.Where(e => orderedIds.Contains(e.Id)), filter)
|
||||
.AsEnumerable()
|
||||
.Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
|
||||
.Where(dto => dto is not null)
|
||||
.ToDictionary(i => i!.Id);
|
||||
|
||||
return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!;
|
||||
}
|
||||
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
@@ -755,16 +774,30 @@ public sealed class BaseItemRepository
|
||||
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var userKeys = item.GetUserDataKeys().ToArray();
|
||||
var retentionDate = (DateTime?)null;
|
||||
await dbContext.UserData
|
||||
.Where(e => e.ItemId == PlaceholderId)
|
||||
.Where(e => userKeys.Contains(e.CustomDataKey))
|
||||
.ExecuteUpdateAsync(
|
||||
e => e
|
||||
.SetProperty(f => f.ItemId, item.Id)
|
||||
.SetProperty(f => f.RetentionDate, retentionDate),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
{
|
||||
var userKeys = item.GetUserDataKeys().ToArray();
|
||||
var retentionDate = (DateTime?)null;
|
||||
|
||||
await dbContext.UserData
|
||||
.Where(e => e.ItemId == PlaceholderId)
|
||||
.Where(e => userKeys.Contains(e.CustomDataKey))
|
||||
.ExecuteUpdateAsync(
|
||||
e => e
|
||||
.SetProperty(f => f.ItemId, item.Id)
|
||||
.SetProperty(f => f.RetentionDate, retentionDate),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Rehydrate the cached userdata
|
||||
item.UserData = await dbContext.UserData
|
||||
.AsNoTracking()
|
||||
.Where(e => e.ItemId == item.Id)
|
||||
.ToArrayAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Migration to fix broken library subtitle download languages.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2026-02-06T20:00:00", nameof(FixLibrarySubtitleDownloadLanguages))]
|
||||
internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine
|
||||
{
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FixLibrarySubtitleDownloadLanguages"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localizationManager">The Localization manager.</param>
|
||||
/// <param name="startupLogger">The startup logger for Startup UI integration.</param>
|
||||
/// <param name="libraryManager">The Library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public FixLibrarySubtitleDownloadLanguages(
|
||||
ILocalizationManager localizationManager,
|
||||
IStartupLogger<FixLibrarySubtitleDownloadLanguages> startupLogger,
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<FixLibrarySubtitleDownloadLanguages> logger)
|
||||
{
|
||||
_localizationManager = localizationManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = startupLogger.With(logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting to fix library subtitle download languages.");
|
||||
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders(false);
|
||||
|
||||
foreach (var virtualFolder in virtualFolders)
|
||||
{
|
||||
var options = virtualFolder.LibraryOptions;
|
||||
if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some virtual folders don't have a proper item id.
|
||||
if (!Guid.TryParse(virtualFolder.ItemId, out var folderId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId);
|
||||
if (collectionFolder is null)
|
||||
{
|
||||
_logger.LogWarning("Could not find collection folder for virtual folder '{LibraryName}' with id '{FolderId}'. Skipping.", virtualFolder.Name, folderId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var fixedLanguages = new List<string>();
|
||||
|
||||
foreach (var language in options.SubtitleDownloadLanguages)
|
||||
{
|
||||
var foundLanguage = _localizationManager.FindLanguageInfo(language)?.ThreeLetterISOLanguageName;
|
||||
if (foundLanguage is not null)
|
||||
{
|
||||
// Converted ISO 639-2/B to T (ger to deu)
|
||||
if (!string.Equals(foundLanguage, language, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("Converted '{Language}' to '{ResolvedLanguage}' in library '{LibraryName}'.", language, foundLanguage, virtualFolder.Name);
|
||||
}
|
||||
|
||||
if (fixedLanguages.Contains(foundLanguage, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("Language '{Language}' already exists for library '{LibraryName}'. Skipping duplicate.", foundLanguage, virtualFolder.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
fixedLanguages.Add(foundLanguage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Could not resolve language '{Language}' in library '{LibraryName}'. Skipping.", language, virtualFolder.Name);
|
||||
}
|
||||
}
|
||||
|
||||
options.SubtitleDownloadLanguages = [.. fixedLanguages];
|
||||
collectionFolder.UpdateLibraryOptions(options);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Library subtitle download languages fixed.");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -464,6 +464,16 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
using (var checkpointConnection = new SqliteConnection($"Filename={libraryDbPath}"))
|
||||
{
|
||||
checkpointConnection.Open();
|
||||
using var cmd = checkpointConnection.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
_logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
|
||||
File.Move(libraryDbPath, libraryDbPath + ".old", true);
|
||||
}
|
||||
@@ -1163,7 +1173,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
Item = null!,
|
||||
ProviderId = e[0],
|
||||
ProviderValue = string.Join('|', e.Skip(1))
|
||||
}).ToArray();
|
||||
})
|
||||
.DistinctBy(e => e.ProviderId)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (reader.TryGetString(index++, out var imageInfos))
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<VersionPrefix>10.11.7</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1172,11 +1172,18 @@ namespace MediaBrowser.Controller.Entities
|
||||
info.Video3DFormat = video.Video3DFormat;
|
||||
info.Timestamp = video.Timestamp;
|
||||
|
||||
if (video.IsShortcut)
|
||||
if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
|
||||
{
|
||||
info.IsRemote = true;
|
||||
info.Path = video.ShortcutPath;
|
||||
info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
|
||||
var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
|
||||
|
||||
// Only allow remote shortcut paths — local file paths in .strm files
|
||||
// could be used to read arbitrary files from the server.
|
||||
if (shortcutProtocol != MediaProtocol.File)
|
||||
{
|
||||
info.IsRemote = true;
|
||||
info.Path = video.ShortcutPath;
|
||||
info.Protocol = shortcutProtocol;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(info.Container))
|
||||
|
||||
@@ -452,6 +452,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
// That's all the new and changed ones - now see if any have been removed and need cleanup
|
||||
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
|
||||
var shouldRemove = !IsRoot || allowRemoveRoot;
|
||||
var actuallyRemoved = new List<BaseItem>();
|
||||
// If it's an AggregateFolder, don't remove
|
||||
if (shouldRemove && itemsRemoved.Count > 0)
|
||||
{
|
||||
@@ -467,6 +468,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
Logger.LogDebug("Removed item: {Path}", item.Path);
|
||||
|
||||
actuallyRemoved.Add(item);
|
||||
item.SetParent(null);
|
||||
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
|
||||
}
|
||||
@@ -477,6 +479,20 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
LibraryManager.CreateItems(newItems, this, cancellationToken);
|
||||
}
|
||||
|
||||
// After removing items, reattach any detached user data to remaining children
|
||||
// that share the same user data keys (eg. same episode replaced with a new file).
|
||||
if (actuallyRemoved.Count > 0)
|
||||
{
|
||||
var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet();
|
||||
foreach (var child in validChildren)
|
||||
{
|
||||
if (child.GetUserDataKeys().Any(removedKeys.Contains))
|
||||
{
|
||||
await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -201,12 +201,17 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
|
||||
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||
{
|
||||
if (series is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetEpisodes()
|
||||
{
|
||||
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
|
||||
return GetEpisodes(Series, null, null, new DtoOptions(true), true);
|
||||
}
|
||||
|
||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<VersionPrefix>10.11.7</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -33,18 +33,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
public partial class EncodingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The codec validation regex.
|
||||
/// The codec validation regex string.
|
||||
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
|
||||
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
||||
/// This should matches all common valid codecs.
|
||||
/// </summary>
|
||||
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
||||
public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
||||
|
||||
/// <summary>
|
||||
/// The level validation regex.
|
||||
/// The level validation regex string.
|
||||
/// This regular expression matches strings representing a double.
|
||||
/// </summary>
|
||||
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
|
||||
public const string LevelValidationRegexStr = @"-?[0-9]+(?:\.[0-9]+)?";
|
||||
|
||||
private const string _defaultMjpegEncoder = "mjpeg";
|
||||
|
||||
@@ -85,8 +85,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
|
||||
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
|
||||
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
|
||||
|
||||
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
|
||||
private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0);
|
||||
|
||||
private static readonly string[] _videoProfilesH264 =
|
||||
[
|
||||
@@ -180,6 +179,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
RemoveHdr10Plus,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The codec validation regex.
|
||||
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
|
||||
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
||||
/// This should matches all common valid codecs.
|
||||
/// </summary>
|
||||
[GeneratedRegex(@"^[a-zA-Z0-9\-\._,|]{0,40}$")]
|
||||
public static partial Regex ContainerValidationRegex();
|
||||
|
||||
[GeneratedRegex(@"\s+")]
|
||||
private static partial Regex WhiteSpaceRegex();
|
||||
|
||||
@@ -476,7 +484,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return GetMjpegEncoder(state, encodingOptions);
|
||||
}
|
||||
|
||||
if (_containerValidationRegex.IsMatch(codec))
|
||||
if (ContainerValidationRegex().IsMatch(codec))
|
||||
{
|
||||
return codec.ToLowerInvariant();
|
||||
}
|
||||
@@ -517,7 +525,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public static string GetInputFormat(string container)
|
||||
{
|
||||
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
|
||||
if (string.IsNullOrEmpty(container) || !ContainerValidationRegex().IsMatch(container))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -735,7 +743,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
var codec = state.OutputAudioCodec;
|
||||
|
||||
if (!_containerValidationRegex.IsMatch(codec))
|
||||
if (!ContainerValidationRegex().IsMatch(codec))
|
||||
{
|
||||
codec = "aac";
|
||||
}
|
||||
@@ -1267,6 +1275,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
// Use analyzeduration also for subtitle streams to improve resolution detection with streams inside MKS files
|
||||
var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state);
|
||||
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
||||
{
|
||||
arg.Append(' ').Append(analyzeDurationArgument);
|
||||
}
|
||||
|
||||
// Apply probesize, too, if configured
|
||||
var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg();
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument))
|
||||
{
|
||||
arg.Append(' ').Append(ffmpegProbeSizeArgument);
|
||||
}
|
||||
|
||||
// Also seek the external subtitles stream.
|
||||
var seekSubParam = GetFastSeekCommandLineParameter(state, options, segmentContainer);
|
||||
if (!string.IsNullOrEmpty(seekSubParam))
|
||||
@@ -1768,38 +1790,40 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
|
||||
{
|
||||
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
|
||||
if (!double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
|
||||
{
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Transcode to level 5.3 (15) and lower for maximum compatibility.
|
||||
// https://en.wikipedia.org/wiki/AV1#Levels
|
||||
if (requestLevel < 0 || requestLevel >= 15)
|
||||
{
|
||||
// Transcode to level 5.3 (15) and lower for maximum compatibility.
|
||||
// https://en.wikipedia.org/wiki/AV1#Levels
|
||||
if (requestLevel < 0 || requestLevel >= 15)
|
||||
{
|
||||
return "15";
|
||||
}
|
||||
return "15";
|
||||
}
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||
}
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Transcode to level 5.0 and lower for maximum compatibility.
|
||||
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
|
||||
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
||||
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
|
||||
if (requestLevel < 0 || requestLevel >= 150)
|
||||
{
|
||||
// Transcode to level 5.0 and lower for maximum compatibility.
|
||||
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
|
||||
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
||||
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
|
||||
if (requestLevel < 0 || requestLevel >= 150)
|
||||
{
|
||||
return "150";
|
||||
}
|
||||
return "150";
|
||||
}
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
}
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Transcode to level 5.1 and lower for maximum compatibility.
|
||||
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
|
||||
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
||||
if (requestLevel < 0 || requestLevel >= 51)
|
||||
{
|
||||
// Transcode to level 5.1 and lower for maximum compatibility.
|
||||
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
|
||||
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
||||
if (requestLevel < 0 || requestLevel >= 51)
|
||||
{
|
||||
return "51";
|
||||
}
|
||||
return "51";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2189,12 +2213,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
var level = state.GetRequestedLevel(targetVideoCodec);
|
||||
var level = NormalizeTranscodingLevel(state, state.GetRequestedLevel(targetVideoCodec));
|
||||
|
||||
if (!string.IsNullOrEmpty(level))
|
||||
{
|
||||
level = NormalizeTranscodingLevel(state, level);
|
||||
|
||||
// libx264, QSV, AMF can adjust the given level to match the output.
|
||||
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -6360,17 +6382,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format
|
||||
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
|
||||
&& ((videoStream.Profile?.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) ?? false)
|
||||
|| (videoStream.Profile?.Contains("4:4:4", StringComparison.OrdinalIgnoreCase) ?? false)))
|
||||
{
|
||||
if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase)
|
||||
|| videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase))
|
||||
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
|
||||
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
|
||||
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
|
||||
{
|
||||
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
|
||||
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
|
||||
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7118,9 +7138,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
|
||||
private string GetFfmpegAnalyzeDurationArg(EncodingJobInfo state)
|
||||
{
|
||||
var inputModifier = string.Empty;
|
||||
var analyzeDurationArgument = string.Empty;
|
||||
|
||||
// Apply -analyzeduration as per the environment variable,
|
||||
@@ -7136,6 +7155,26 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
|
||||
return analyzeDurationArgument;
|
||||
}
|
||||
|
||||
private string GetFfmpegProbesizeArg()
|
||||
{
|
||||
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSize))
|
||||
{
|
||||
return $"-probesize {ffmpegProbeSize}";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
|
||||
{
|
||||
var inputModifier = string.Empty;
|
||||
var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state);
|
||||
|
||||
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
||||
{
|
||||
inputModifier += " " + analyzeDurationArgument;
|
||||
@@ -7144,11 +7183,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
inputModifier = inputModifier.Trim();
|
||||
|
||||
// Apply -probesize if configured
|
||||
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
|
||||
var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg();
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSize))
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument))
|
||||
{
|
||||
inputModifier += $" -probesize {ffmpegProbeSize}";
|
||||
inputModifier += " " + ffmpegProbeSizeArgument;
|
||||
}
|
||||
|
||||
var userAgentParam = GetUserAgentParam(state);
|
||||
@@ -7188,8 +7227,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
|
||||
}
|
||||
|
||||
int readrate = 0;
|
||||
if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp)
|
||||
{
|
||||
readrate = 1;
|
||||
inputModifier += " -re";
|
||||
}
|
||||
else if (encodingOptions.EnableSegmentDeletion
|
||||
@@ -7200,7 +7241,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
// Set an input read rate limit 10x for using SegmentDeletion with stream-copy
|
||||
// to prevent ffmpeg from exiting prematurely (due to fast drive)
|
||||
inputModifier += " -readrate 10";
|
||||
readrate = 10;
|
||||
inputModifier += $" -readrate {readrate}";
|
||||
}
|
||||
|
||||
// Set a larger catchup value to revert to the old behavior,
|
||||
// otherwise, remuxing might stall due to this new option
|
||||
if (readrate > 0 && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateCatchupOption)
|
||||
{
|
||||
inputModifier += $" -readrate_catchup {readrate * 100}";
|
||||
}
|
||||
|
||||
var flags = new List<string>();
|
||||
|
||||
@@ -350,5 +350,12 @@ namespace MediaBrowser.Controller.Session
|
||||
/// <param name="sessionIdOrPlaySessionId">The session id or playsession id.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dto for session info.
|
||||
/// </summary>
|
||||
/// <param name="sessionInfo">The session info.</param>
|
||||
/// <returns><see cref="SessionInfoDto"/> of the session.</returns>
|
||||
SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,7 +693,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
[GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
private static partial Regex CodecRegex();
|
||||
|
||||
[GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
[GeneratedRegex("^\\s\\S{2,3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
private static partial Regex FilterRegex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
{
|
||||
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -423,9 +423,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!File.Exists(outputPath))
|
||||
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
{
|
||||
failed = true;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Deleting converted subtitle due to failure: {Path}", outputPath);
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed)
|
||||
@@ -499,7 +512,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (File.Exists(outputPath))
|
||||
if (File.Exists(outputPath) && _fileSystem.GetFileInfo(outputPath).Length > 0)
|
||||
{
|
||||
releaser.Dispose();
|
||||
continue;
|
||||
@@ -556,7 +569,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var outputPaths = new List<string>();
|
||||
var args = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i {0} -copyts",
|
||||
"-i {0}",
|
||||
inputPath);
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
@@ -581,7 +594,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
outputPaths.Add(outputPath);
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
|
||||
" -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
|
||||
streamIndex,
|
||||
outputCodec,
|
||||
outputPath);
|
||||
@@ -600,7 +613,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var outputPaths = new List<string>();
|
||||
var args = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i {0} -copyts",
|
||||
"-i {0}",
|
||||
inputPath);
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
@@ -626,7 +639,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
outputPaths.Add(outputPath);
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
|
||||
" -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
|
||||
streamIndex,
|
||||
outputCodec,
|
||||
outputPath);
|
||||
@@ -713,10 +726,24 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
foreach (var outputPath in outputPaths)
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
{
|
||||
_logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
|
||||
failed = true;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -755,7 +782,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
{
|
||||
var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
||||
|
||||
@@ -857,9 +884,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
else if (!File.Exists(outputPath))
|
||||
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
||||
{
|
||||
failed = true;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<VersionPrefix>10.11.7</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -262,9 +262,28 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
private void FetchShortcutInfo(BaseItem item)
|
||||
{
|
||||
item.ShortcutPath = File.ReadAllLines(item.Path)
|
||||
var shortcutPath = File.ReadAllLines(item.Path)
|
||||
.Select(NormalizeStrmLine)
|
||||
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#'));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(shortcutPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow remote URLs in .strm files to prevent local file access
|
||||
if (Uri.TryCreate(shortcutPath, UriKind.Absolute, out var uri)
|
||||
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
item.ShortcutPath = shortcutPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Ignoring invalid or non-remote .strm path in {File}: {Path}", item.Path, shortcutPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -518,7 +518,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
return null;
|
||||
}
|
||||
|
||||
return _tmDbClient.GetImageUrl(size, path, true).ToString();
|
||||
// Use "original" as default size if size is null or empty to prevent malformed URLs
|
||||
var imageSize = string.IsNullOrEmpty(size) ? "original" : size;
|
||||
|
||||
return _tmDbClient.GetImageUrl(imageSize, path, true).ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <returns>The Jellyfin person type.</returns>
|
||||
public static PersonKind MapCrewToPersonType(Crew crew)
|
||||
{
|
||||
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
|
||||
if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase)
|
||||
&& crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return PersonKind.Director;
|
||||
@@ -82,7 +82,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
}
|
||||
|
||||
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)
|
||||
&& crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase))
|
||||
&& (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return PersonKind.Writer;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -32,6 +33,7 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
private readonly ILibraryMonitor _monitor;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly HashSet<string> _allowedSubtitleFormats;
|
||||
|
||||
private readonly ISubtitleProvider[] _subtitleProviders;
|
||||
|
||||
@@ -41,7 +43,8 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
ILibraryMonitor monitor,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
ILocalizationManager localizationManager,
|
||||
IEnumerable<ISubtitleProvider> subtitleProviders)
|
||||
IEnumerable<ISubtitleProvider> subtitleProviders,
|
||||
NamingOptions namingOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
@@ -51,6 +54,9 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
_subtitleProviders = subtitleProviders
|
||||
.OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
|
||||
.ToArray();
|
||||
_allowedSubtitleFormats = new HashSet<string>(
|
||||
namingOptions.SubtitleFileExtensions.Select(e => e.TrimStart('.')),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -171,6 +177,12 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
/// <inheritdoc />
|
||||
public Task UploadSubtitle(Video video, SubtitleResponse response)
|
||||
{
|
||||
var format = response.Format;
|
||||
if (string.IsNullOrEmpty(format) || !_allowedSubtitleFormats.Contains(format))
|
||||
{
|
||||
throw new ArgumentException($"Unsupported subtitle format: '{format}'");
|
||||
}
|
||||
|
||||
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
|
||||
return TrySaveSubtitle(video, libraryOptions, response);
|
||||
}
|
||||
@@ -193,7 +205,13 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
}
|
||||
|
||||
var savePaths = new List<string>();
|
||||
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
|
||||
var language = response.Language.ToLowerInvariant();
|
||||
if (language.AsSpan().IndexOfAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) >= 0)
|
||||
{
|
||||
throw new ArgumentException("Language contains invalid characters.");
|
||||
}
|
||||
|
||||
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + language;
|
||||
|
||||
if (response.IsForced)
|
||||
{
|
||||
@@ -221,15 +239,22 @@ namespace MediaBrowser.Providers.Subtitles
|
||||
|
||||
private async Task TrySaveToFiles(Stream stream, List<string> savePaths, Video video, string extension)
|
||||
{
|
||||
if (!_allowedSubtitleFormats.Contains("." + extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException($"Invalid subtitle format: {extension}");
|
||||
}
|
||||
|
||||
List<Exception>? exs = null;
|
||||
|
||||
foreach (var savePath in savePaths)
|
||||
{
|
||||
var path = savePath + "." + extension;
|
||||
var path = Path.GetFullPath(savePath + "." + extension);
|
||||
try
|
||||
{
|
||||
if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)
|
||||
|| path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
|
||||
var containingFolder = video.ContainingFolderPath + Path.DirectorySeparatorChar;
|
||||
var metadataFolder = video.GetInternalMetadataPath() + Path.DirectorySeparatorChar;
|
||||
if (path.StartsWith(containingFolder, StringComparison.Ordinal)
|
||||
|| path.StartsWith(metadataFolder, StringComparison.Ordinal))
|
||||
{
|
||||
var fileExists = File.Exists(path);
|
||||
var counter = 0;
|
||||
|
||||
@@ -547,7 +547,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
|
||||
}
|
||||
|
||||
if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection))
|
||||
if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
|
||||
{
|
||||
writer.WriteElementString("collectionnumber", tmdbCollection);
|
||||
writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.11.6")]
|
||||
[assembly: AssemblyFileVersion("10.11.6")]
|
||||
[assembly: AssemblyVersion("10.11.7")]
|
||||
[assembly: AssemblyFileVersion("10.11.7")]
|
||||
|
||||
@@ -24,61 +24,29 @@ public class SkiaEncoder : IImageEncoder
|
||||
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||
private readonly ILogger<SkiaEncoder> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private static readonly SKImageFilter _imageFilter;
|
||||
private static readonly SKTypeface[] _typefaces;
|
||||
private static readonly SKTypeface?[] _typefaces = InitializeTypefaces();
|
||||
private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution(
|
||||
new SKSizeI(3, 3),
|
||||
[
|
||||
0, -.1f, 0,
|
||||
-.1f, 1.4f, -.1f,
|
||||
0, -.1f, 0
|
||||
],
|
||||
1f,
|
||||
0f,
|
||||
new SKPointI(1, 1),
|
||||
SKShaderTileMode.Clamp,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// The default sampling options, equivalent to old high quality filter settings when upscaling.
|
||||
/// </summary>
|
||||
public static readonly SKSamplingOptions UpscaleSamplingOptions;
|
||||
public static readonly SKSamplingOptions UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
|
||||
|
||||
/// <summary>
|
||||
/// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling.
|
||||
/// </summary>
|
||||
public static readonly SKSamplingOptions DefaultSamplingOptions;
|
||||
|
||||
#pragma warning disable CA1810
|
||||
static SkiaEncoder()
|
||||
#pragma warning restore CA1810
|
||||
{
|
||||
var kernel = new[]
|
||||
{
|
||||
0, -.1f, 0,
|
||||
-.1f, 1.4f, -.1f,
|
||||
0, -.1f, 0,
|
||||
};
|
||||
|
||||
var kernelSize = new SKSizeI(3, 3);
|
||||
var kernelOffset = new SKPointI(1, 1);
|
||||
_imageFilter = SKImageFilter.CreateMatrixConvolution(
|
||||
kernelSize,
|
||||
kernel,
|
||||
1f,
|
||||
0f,
|
||||
kernelOffset,
|
||||
SKShaderTileMode.Clamp,
|
||||
true);
|
||||
|
||||
// Initialize the list of typefaces
|
||||
// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
|
||||
// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F)
|
||||
_typefaces =
|
||||
[
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ノ'), // CJK Japanese
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew
|
||||
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic
|
||||
SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font
|
||||
];
|
||||
|
||||
// use cubic for upscaling
|
||||
UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
|
||||
// use bilinear for everything else
|
||||
DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
|
||||
}
|
||||
public static readonly SKSamplingOptions DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
|
||||
@@ -132,7 +100,7 @@ public class SkiaEncoder : IImageEncoder
|
||||
/// <summary>
|
||||
/// Gets the default typeface to use.
|
||||
/// </summary>
|
||||
public static SKTypeface DefaultTypeFace => _typefaces.Last();
|
||||
public static SKTypeface? DefaultTypeFace => _typefaces.Last();
|
||||
|
||||
/// <summary>
|
||||
/// Check if the native lib is available.
|
||||
@@ -152,6 +120,40 @@ public class SkiaEncoder : IImageEncoder
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the list of typefaces
|
||||
/// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
|
||||
/// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F).
|
||||
/// </summary>
|
||||
/// <returns>The list of typefaces.</returns>
|
||||
private static SKTypeface?[] InitializeTypefaces()
|
||||
{
|
||||
int[] chars = [
|
||||
'鸡', // CJK Simplified Chinese
|
||||
'雞', // CJK Traditional Chinese
|
||||
'ノ', // CJK Japanese
|
||||
'각', // CJK Korean
|
||||
128169, // Emojis, 128169 is the Pile of Poo (💩) emoji
|
||||
'ז', // Hebrew
|
||||
'ي' // Arabic
|
||||
];
|
||||
var fonts = new List<SKTypeface>(chars.Length + 1);
|
||||
foreach (var ch in chars)
|
||||
{
|
||||
var font = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ch);
|
||||
if (font is not null)
|
||||
{
|
||||
fonts.Add(font);
|
||||
}
|
||||
}
|
||||
|
||||
// Default font
|
||||
fonts.Add(SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright)
|
||||
?? SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'a'));
|
||||
|
||||
return fonts.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
|
||||
/// </summary>
|
||||
@@ -809,7 +811,7 @@ public class SkiaEncoder : IImageEncoder
|
||||
{
|
||||
foreach (var typeface in _typefaces)
|
||||
{
|
||||
if (typeface.ContainsGlyphs(c))
|
||||
if (typeface is not null && typeface.ContainsGlyphs(c))
|
||||
{
|
||||
return typeface;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.11.6</VersionPrefix>
|
||||
<VersionPrefix>10.11.7</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -156,6 +156,13 @@ namespace Jellyfin.LiveTv.IO
|
||||
if (mediaSource.ReadAtNativeFramerate)
|
||||
{
|
||||
inputModifier += " -re";
|
||||
|
||||
// Set a larger catchup value to revert to the old behavior,
|
||||
// otherwise, remuxing might stall due to this new option
|
||||
if (_mediaEncoder.EncoderVersion >= new Version(8, 0))
|
||||
{
|
||||
inputModifier += " -readrate_catchup 100";
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaSource.RequiresLooping)
|
||||
|
||||
@@ -93,6 +93,13 @@ namespace Jellyfin.LiveTv.TunerHosts
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
|
||||
{
|
||||
if (!IsValidChannelUrl(trimmedLine))
|
||||
{
|
||||
_logger.LogWarning("Skipping M3U channel entry with non-HTTP path: {Path}", trimmedLine);
|
||||
extInf = string.Empty;
|
||||
continue;
|
||||
}
|
||||
|
||||
var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine);
|
||||
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -247,6 +254,16 @@ namespace Jellyfin.LiveTv.TunerHosts
|
||||
return numberString;
|
||||
}
|
||||
|
||||
private static bool IsValidChannelUrl(string url)
|
||||
{
|
||||
return Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
||||
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(uri.Scheme, "udp", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsValidChannelNumber(string numberString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(numberString)
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
|
||||
[InlineData("/media/movies/#recycle", true)]
|
||||
[InlineData("thumbs.db", true)]
|
||||
[InlineData(@"C:\media\movies\movie.avi", false)]
|
||||
[InlineData("/media/.hiddendir/file.mp4", true)]
|
||||
[InlineData("/media/.hiddendir/file.mp4", false)]
|
||||
[InlineData("/media/dir/.hiddenfile.mp4", true)]
|
||||
[InlineData("/media/dir/._macjunk.mp4", true)]
|
||||
[InlineData("/volume1/video/Series/@eaDir", true)]
|
||||
@@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
|
||||
[InlineData("/media/music/Foo B.A.R", false)]
|
||||
[InlineData("/media/music/Foo B.A.R.", false)]
|
||||
[InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
|
||||
public void PathIgnored(string path, bool expected)
|
||||
public void PathIgnored(string path, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user