mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-04 06:48:35 +01:00
Compare commits
14 Commits
standards-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21c0a35edf | ||
|
|
cf88058099 | ||
|
|
5ee9e79da2 | ||
|
|
5ed7798c36 | ||
|
|
b71b4cc26f | ||
|
|
7185257da5 | ||
|
|
d4c962f6e4 | ||
|
|
52cf8d1ba4 | ||
|
|
081f0ef4a0 | ||
|
|
cc5fb3f1ee | ||
|
|
9ab7cc0fe9 | ||
|
|
285fc1b9f6 | ||
|
|
5ce7170813 | ||
|
|
e627c723e2 |
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
@@ -32,13 +32,13 @@ jobs:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/autobuild@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
|
||||
|
||||
4
.github/workflows/ci-compat.yml
vendored
4
.github/workflows/ci-compat.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
2
.github/workflows/ci-format.yml
vendored
2
.github/workflows/ci-format.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
format-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
with:
|
||||
|
||||
4
.github/workflows/commands.yml
vendored
4
.github/workflows/commands.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
|
||||
|
||||
2
.github/workflows/issue-template-check.yml
vendored
2
.github/workflows/issue-template-check.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
|
||||
|
||||
2
.github/workflows/openapi-generate.yml
vendored
2
.github/workflows/openapi-generate.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
repository: ${{ inputs.repository }}
|
||||
|
||||
2
.github/workflows/openapi-pull-request.yml
vendored
2
.github/workflows/openapi-pull-request.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
base_ref: ${{ steps.ancestor.outputs.base_ref }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
36
.github/workflows/pull-request-standards.yml
vendored
36
.github/workflows/pull-request-standards.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: Standards Check
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/CLAUDE.md'
|
||||
- '**/AGENTS.md'
|
||||
- 'docs/superpowers/**'
|
||||
|
||||
jobs:
|
||||
close:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: 'This PR does not follow our contributing guidelines. https://jellyfin.org/docs/general/contributing/'
|
||||
});
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['invalid']
|
||||
});
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
4
.github/workflows/release-bump-version.yaml
vendored
4
.github/workflows/release-bump-version.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
yq-version: v4.9.8
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
||||
item.CustomRating = request.CustomRating;
|
||||
|
||||
var currentTags = item.Tags;
|
||||
var newTags = request.Tags.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var newTags = request.Tags.Select(t => t.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
var removedTags = currentTags.Except(newTags).ToList();
|
||||
var addedTags = newTags.Except(currentTags).ToList();
|
||||
item.Tags = newTags;
|
||||
|
||||
@@ -318,9 +318,6 @@ public class ItemsController : BaseJellyfinApiController
|
||||
}
|
||||
else if (folder is ICollectionFolder)
|
||||
{
|
||||
// When the client doesn't specify recursive/includeItemTypes, force the query
|
||||
// through the database path where all filters (IsHD, genres, etc.) are applied.
|
||||
recursive ??= true;
|
||||
if (includeItemTypes.Length == 0)
|
||||
{
|
||||
includeItemTypes = collectionType switch
|
||||
@@ -330,6 +327,13 @@ public class ItemsController : BaseJellyfinApiController
|
||||
_ => []
|
||||
};
|
||||
}
|
||||
|
||||
// When the client doesn't specify recursive/includeItemTypes, force the query
|
||||
// through the database path where all filters (IsHD, genres, etc.) are applied.
|
||||
if (includeItemTypes.Length > 0)
|
||||
{
|
||||
recursive ??= true;
|
||||
}
|
||||
}
|
||||
|
||||
if (item is not UserRootFolder
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
name = name.Trim();
|
||||
var current = item.Tags;
|
||||
|
||||
if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
@@ -102,13 +104,10 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
|
||||
if (shouldExtractOneByOne && !inputFile.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var attachment in mediaSource.MediaAttachments)
|
||||
{
|
||||
if (!string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await ExtractAttachment(inputFile, mediaSource, attachment, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
await ExtractAllAttachmentsIndividuallyInternal(
|
||||
inputFile,
|
||||
mediaSource,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -119,6 +118,140 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAllAttachmentsIndividuallyInternal(
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||
|
||||
var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
|
||||
if (outputFolder is null)
|
||||
{
|
||||
_logger.LogDebug("Skipping attachment extraction for input {InputFile}: MediaSource Id is not a GUID.", inputFile);
|
||||
return;
|
||||
}
|
||||
|
||||
using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
Directory.CreateDirectory(outputFolder);
|
||||
|
||||
var dumpArgs = new StringBuilder();
|
||||
var missingPaths = new List<string>();
|
||||
foreach (var attachment in mediaSource.MediaAttachments)
|
||||
{
|
||||
if (string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var indexName = attachment.Index.ToString(CultureInfo.InvariantCulture);
|
||||
var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, attachment.FileName ?? indexName)
|
||||
?? _pathManager.GetAttachmentPath(mediaSource.Id, indexName)!;
|
||||
if (File.Exists(attachmentPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dumpArgs.AppendFormat(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:{0} \"{1}\" ",
|
||||
attachment.Index,
|
||||
EncodingUtils.NormalizePath(attachmentPath));
|
||||
missingPaths.Add(attachmentPath);
|
||||
}
|
||||
|
||||
if (missingPaths.Count == 0)
|
||||
{
|
||||
// Skip extraction if all files already exist
|
||||
return;
|
||||
}
|
||||
|
||||
var hasVideoOrAudioStream = mediaSource.MediaStreams
|
||||
.Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio);
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}{1} -i {2} {3}",
|
||||
dumpArgs,
|
||||
inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
|
||||
inputPath,
|
||||
hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
|
||||
|
||||
int exitCode;
|
||||
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = processArgs,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
{
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Start();
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
|
||||
if (exitCode != 0 && (hasVideoOrAudioStream || exitCode != 1))
|
||||
{
|
||||
failed = true;
|
||||
|
||||
foreach (var path in missingPaths)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(path);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted attachment {Path}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!failed && missingPaths.Exists(p => !File.Exists(p)))
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (failed)
|
||||
{
|
||||
_logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputFolder);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputFolder));
|
||||
}
|
||||
|
||||
_logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAllAttachmentsInternal(
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
|
||||
@@ -43,6 +43,7 @@ public class EncodingOptions
|
||||
VppTonemappingContrast = 1;
|
||||
H264Crf = 23;
|
||||
H265Crf = 28;
|
||||
EncoderPreset = EncoderPreset.auto;
|
||||
DeinterlaceDoubleRate = false;
|
||||
DeinterlaceMethod = DeinterlaceMethod.yadif;
|
||||
EnableDecodingColorDepth10Hevc = true;
|
||||
@@ -217,7 +218,7 @@ public class EncodingOptions
|
||||
/// <summary>
|
||||
/// Gets or sets the encoder preset.
|
||||
/// </summary>
|
||||
public EncoderPreset? EncoderPreset { get; set; }
|
||||
public EncoderPreset EncoderPreset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the framerate is doubled when deinterlacing.
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
var posters = movie.Images.Posters;
|
||||
var backdrops = movie.Images.Backdrops;
|
||||
var logos = movie.Images.Logos;
|
||||
var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0);
|
||||
var remoteImages = new List<RemoteImageInfo>((posters?.Count ?? 0) + (backdrops?.Count ?? 0) + (logos?.Count ?? 0));
|
||||
|
||||
if (posters is not null)
|
||||
{
|
||||
|
||||
@@ -210,16 +210,19 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not yet processed
|
||||
if (episode.SeasonId.IsEmpty())
|
||||
// Episode has been processed and linked to a season, only needs a virtual season
|
||||
// if it isn't already linked to a known physical season by ID or path
|
||||
if (!episode.SeasonId.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
return !physicalSeasonIds.Contains(episode.SeasonId)
|
||||
&& !physicalSeasonPaths.Contains(System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty);
|
||||
}
|
||||
|
||||
// Episode has been processed, only needs a virtual season if it isn't
|
||||
// already linked to a known physical season by ID or path
|
||||
return !physicalSeasonIds.Contains(episode.SeasonId)
|
||||
&& !physicalSeasonPaths.Contains(System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty);
|
||||
// Episode not yet linked, check if it's in a physical season folder
|
||||
// If yes then skip it, processing not finished
|
||||
// If no then include it, needs Season Unknown
|
||||
var episodeDirectory = System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty;
|
||||
return !physicalSeasonPaths.Contains(episodeDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user