diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e1900d5833..2e39990b6f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ # Joshua must review all changes to deployment and build.sh -.ci/* @joshuaboniface -deployment/* @joshuaboniface -build.sh @joshuaboniface +.ci/* @dannymichel +deployment/* @dannymichel +build.sh @dannymichel diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml index 20294843d5..4ec7a741a4 100644 --- a/.github/workflows/automation.yml +++ b/.github/workflows/automation.yml @@ -11,7 +11,7 @@ jobs: label: name: Labeling runs-on: ubuntu-latest - if: ${{ github.repository == 'jellyfin/jellyfin' }} + if: ${{ github.repository == 'vesoapp/veso' }} steps: - name: Apply label uses: eps1lon/actions-label-merge-conflict@v2.0.1 @@ -23,7 +23,7 @@ jobs: project: name: Project board runs-on: ubuntu-latest - if: ${{ github.repository == 'jellyfin/jellyfin' }} + if: ${{ github.repository == 'vesoapp/veso' }} steps: - name: Remove from 'Current Release' project uses: alex-page/github-project-automation-plus@v0.8.1 diff --git a/.gitignore b/.gitignore index c2ae76c1e3..810428809b 100644 --- a/.gitignore +++ b/.gitignore @@ -281,3 +281,4 @@ apiclient/generated # Omnisharp crash logs mono_crash.*.json +.DS_Store diff --git a/Dockerfile b/Dockerfile index 219b958935..5c71716f3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,8 @@ ARG DOTNET_VERSION=6.0 FROM node:lts-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ - && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ - && cd jellyfin-web-* \ + && curl -L https://github.com/vesoapp/veso-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && cd veso-web-* \ && npm ci --no-audit --unsafe-perm \ && mv dist /dist diff --git a/Dockerfile.arm b/Dockerfile.arm index 8e0ba7af53..3b30f0d252 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -8,8 +8,8 @@ ARG DOTNET_VERSION=6.0 FROM node:lts-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ - && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ - && cd jellyfin-web-* \ + && curl -L https://github.com/vesoapp/veso-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && cd veso-web-* \ && npm ci --no-audit --unsafe-perm \ && mv dist /dist diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 790be1c39d..ac8f886d3e 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -8,8 +8,8 @@ ARG DOTNET_VERSION=6.0 FROM node:lts-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ - && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ - && cd jellyfin-web-* \ + && curl -L https://github.com/vesoapp/veso-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && cd veso-web-* \ && npm ci --no-audit --unsafe-perm \ && mv dist /dist diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 909972469e..089cd9fe88 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -83,7 +83,6 @@ using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; -using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; @@ -633,8 +632,7 @@ protected virtual void RegisterServices(IServiceCollection serviceCollection) serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 0d1029882b..853c03d654 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -19,6 +19,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; @@ -56,6 +57,7 @@ public class SessionManager : ISessionManager, IDisposable private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerApplicationHost _appHost; private readonly IDeviceManager _deviceManager; + private readonly IServerConfigurationManager _config; /// /// The active connections. @@ -78,7 +80,8 @@ public class SessionManager : ISessionManager, IDisposable IImageProcessor imageProcessor, IServerApplicationHost appHost, IDeviceManager deviceManager, - IMediaSourceManager mediaSourceManager) + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager config) { _logger = logger; _eventManager = eventManager; @@ -91,6 +94,7 @@ public class SessionManager : ISessionManager, IDisposable _appHost = appHost; _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; + _config = config; _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated; } @@ -728,12 +732,22 @@ private void OnPlaybackStart(User user, BaseItem item) { var data = _userDataManager.GetUserData(user, item); - data.PlayCount++; - data.LastPlayedDate = DateTime.UtcNow; + if (!_config.Configuration.UpdateLastPlayedAndPlayCountOnPlayCompletion) + { + data.PlayCount++; + data.LastPlayedDate = DateTime.UtcNow; + } - if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume) + if (item.SupportsPlayedStatus) { - data.Played = true; + if (!item.SupportsPositionTicksResume) + { + data.Played = true; + } + else if (_config.Configuration.MarkResumableItemUnplayedOnPlay) + { + data.Played = false; + } } else { @@ -1008,6 +1022,11 @@ private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bo if (positionTicks.HasValue) { playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value); + if (playedToCompletion && _config.Configuration.UpdateLastPlayedAndPlayCountOnPlayCompletion) + { + data.PlayCount++; + data.LastPlayedDate = DateTime.UtcNow; + } } else { diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 6005896ad9..14d82fe623 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -260,6 +260,30 @@ Episode GetEpisode() consideredEpisodes.Add(lastWatchedEpisode); } + if (rewatching) + { + lastQuery.ParentIndexNumberNotEquals = null; + lastQuery.ParentIndexNumber = 0; + Episode lastWatchedSpecialEpisode = _libraryManager.GetItemList(lastQuery).Cast().FirstOrDefault(); + + if (lastWatchedEpisode != null && lastWatchedSpecialEpisode != null) + { + // both regular and special episodes have been watched + // compare viewing dates to find latest watched + // if special was more recently watched, replace lastWatchedEpisode with special + var userDataRegular = _userDataManager.GetUserData(user, lastWatchedEpisode); + var userDataSpecial = _userDataManager.GetUserData(user, lastWatchedSpecialEpisode); + if (userDataSpecial.LastPlayedDate > userDataRegular.LastPlayedDate) + { + lastWatchedEpisode = lastWatchedSpecialEpisode; + // get next regular episode based on recently watched special + nextQuery.MinSortName = null; + nextQuery.MinPremiereDate = lastWatchedEpisode.PremiereDate; + nextEpisode = _libraryManager.GetItemList(nextQuery).Cast().FirstOrDefault(); + } + } + } + if (nextEpisode != null) { consideredEpisodes.Add(nextEpisode); diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 83b2262782..58d56e552f 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -33,7 +33,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index f467a60384..60e73c6b2b 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -126,7 +126,7 @@ public QueryResult GetUserItems(Folder queryParent, Folder displayPare private int GetSpecialItemsLimit() { - return 50; + return 100; } private QueryResult GetMovieFolders(Folder parent, User user, InternalItemsQuery query) diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs new file mode 100644 index 0000000000..08ee5c72e5 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; +using Nikse.SubtitleEdit.Core.SubtitleFormats; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + /// + /// Advanced SubStation Alpha subtitle parser. + /// + public class AssParser : SubtitleEditParser + { + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public AssParser(ILogger logger) : base(logger) + { + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs index bd13437fb6..c0023ebf24 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.IO; +using System.Threading; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles @@ -11,15 +12,8 @@ public interface ISubtitleParser /// Parses the specified stream. /// /// The stream. - /// The file extension. + /// The cancellation token. /// SubtitleTrackInfo. - SubtitleTrackInfo Parse(Stream stream, string fileExtension); - - /// - /// Determines whether the file extension is supported by the parser. - /// - /// The file extension. - /// A value indicating whether the file extension is supported. - bool SupportsFileExtension(string fileExtension); + SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs new file mode 100644 index 0000000000..78d54ca51f --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; +using Nikse.SubtitleEdit.Core.SubtitleFormats; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + /// + /// SubRip subtitle parser. + /// + public class SrtParser : SubtitleEditParser + { + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public SrtParser(ILogger logger) : base(logger) + { + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs new file mode 100644 index 0000000000..17c2ae40e0 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; +using Nikse.SubtitleEdit.Core.SubtitleFormats; + +namespace MediaBrowser.MediaEncoding.Subtitles +{ + /// + /// SubStation Alpha subtitle parser. + /// + public class SsaParser : SubtitleEditParser + { + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public SsaParser(ILogger logger) : base(logger) + { + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs index 0d4489517e..52c1b64677 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs @@ -1,12 +1,12 @@ -using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using Jellyfin.Extensions; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; using Nikse.SubtitleEdit.Core.Common; +using ILogger = Microsoft.Extensions.Logging.ILogger; using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat; namespace MediaBrowser.MediaEncoding.Subtitles @@ -14,57 +14,31 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// /// SubStation Alpha subtitle parser. /// - public class SubtitleEditParser : ISubtitleParser + /// The . + public abstract class SubtitleEditParser : ISubtitleParser + where T : SubtitleFormat, new() { - private readonly ILogger _logger; - private readonly Dictionary _subtitleFormats; + private readonly ILogger _logger; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The logger. - public SubtitleEditParser(ILogger logger) + protected SubtitleEditParser(ILogger logger) { _logger = logger; - _subtitleFormats = GetSubtitleFormats() - .Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension)) - .GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase) - .ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase); } /// - public SubtitleTrackInfo Parse(Stream stream, string fileExtension) + public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var subtitle = new Subtitle(); + var subRip = new T(); var lines = stream.ReadAllLines().ToList(); - - if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats)) - { - throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension)); - } - - foreach (var subtitleFormat in subtitleFormats) + subRip.LoadSubtitle(subtitle, lines, "untitled"); + if (subRip.ErrorCount > 0) { - _logger.LogDebug( - "Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser", - fileExtension, - subtitleFormat.Name); - subtitleFormat.LoadSubtitle(subtitle, lines, fileExtension); - if (subtitleFormat.ErrorCount == 0) - { - break; - } - - _logger.LogError( - "{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser", - subtitleFormat.ErrorCount, - fileExtension, - subtitleFormat.Name); - } - - if (subtitle.Paragraphs.Count == 0) - { - throw new ArgumentException("Unsupported format: " + fileExtension); + _logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount); } var trackInfo = new SubtitleTrackInfo(); @@ -83,36 +57,5 @@ public SubtitleTrackInfo Parse(Stream stream, string fileExtension) trackInfo.TrackEvents = trackEvents; return trackInfo; } - - /// - public bool SupportsFileExtension(string fileExtension) - => _subtitleFormats.ContainsKey(fileExtension); - - private IEnumerable GetSubtitleFormats() - { - var subtitleFormats = new List(); - var assembly = typeof(SubtitleFormat).Assembly; - - foreach (var type in assembly.GetTypes()) - { - if (!type.IsSubclassOf(typeof(SubtitleFormat)) || type.IsAbstract) - { - continue; - } - - try - { - // It shouldn't be null, but the exception is caught if it is - var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!; - subtitleFormats.Add(subtitleFormat); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to create instance of the subtitle format {SubtitleFormatType}", type.Name); - } - } - - return subtitleFormats; - } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 9185faf67b..33bd0dbeb1 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -35,7 +35,6 @@ public sealed class SubtitleEncoder : ISubtitleEncoder private readonly IMediaEncoder _mediaEncoder; private readonly IHttpClientFactory _httpClientFactory; private readonly IMediaSourceManager _mediaSourceManager; - private readonly ISubtitleParser _subtitleParser; /// /// The _semaphoreLocks. @@ -49,8 +48,7 @@ public sealed class SubtitleEncoder : ISubtitleEncoder IFileSystem fileSystem, IMediaEncoder mediaEncoder, IHttpClientFactory httpClientFactory, - IMediaSourceManager mediaSourceManager, - ISubtitleParser subtitleParser) + IMediaSourceManager mediaSourceManager) { _logger = logger; _appPaths = appPaths; @@ -58,7 +56,6 @@ public sealed class SubtitleEncoder : ISubtitleEncoder _mediaEncoder = mediaEncoder; _httpClientFactory = httpClientFactory; _mediaSourceManager = mediaSourceManager; - _subtitleParser = subtitleParser; } private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); @@ -76,7 +73,8 @@ public sealed class SubtitleEncoder : ISubtitleEncoder try { - var trackInfo = _subtitleParser.Parse(stream, inputFormat); + var reader = GetReader(inputFormat); + var trackInfo = reader.Parse(stream, cancellationToken); FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); @@ -232,8 +230,7 @@ await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) .TrimStart('.'); - // Fallback to ffmpeg conversion - if (!_subtitleParser.SupportsFileExtension(currentFormat)) + if (!TryGetReader(currentFormat, out _)) { // Convert var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt"); @@ -243,10 +240,44 @@ await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true); } - // It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs) + // It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs) return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true); } + private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value) + { + if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) + { + value = new SrtParser(_logger); + return true; + } + + if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)) + { + value = new SsaParser(_logger); + return true; + } + + if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) + { + value = new AssParser(_logger); + return true; + } + + value = null; + return false; + } + + private ISubtitleParser GetReader(string format) + { + if (TryGetReader(format, out var reader)) + { + return reader; + } + + throw new ArgumentException("Unsupported format: " + format); + } + private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value) { if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index e61b896b9d..7b88fc25f9 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -127,6 +127,18 @@ public ServerConfiguration() /// The sort remove words. public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; + /// + /// Gets or sets a value indicating whether to mark a resumable item being played as unplayed when the play starts. + /// + /// true if this resumable item should be marked unplayed; otherwise, false item play state will not change when play starts. + public bool MarkResumableItemUnplayedOnPlay { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to set LastPlayed time and increment playcount only when played to completion, or on every play start. + /// + /// true if playcount and lastplayed update on play completion; otherwise, false playcount and lastplayed update on play start. + public bool UpdateLastPlayedAndPlayCountOnPlayCompletion { get; set; } = false; + /// /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. /// diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index b121a29058..da1e6c3d38 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -436,9 +436,9 @@ private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSou { containerSupported = true; - videoSupported = videoStream != null && profile.SupportsVideoCodec(videoStream.Codec); + videoSupported = videoStream == null || profile.SupportsVideoCodec(videoStream.Codec); - audioSupported = audioStream != null && profile.SupportsAudioCodec(audioStream.Codec); + audioSupported = audioStream == null || profile.SupportsAudioCodec(audioStream.Codec); if (videoSupported && audioSupported) { @@ -447,18 +447,17 @@ private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSou } } - var list = new List(); if (!containerSupported) { reasons |= TranscodeReason.ContainerNotSupported; } - if (videoStream != null && !videoSupported) + if (!videoSupported) { reasons |= TranscodeReason.VideoCodecNotSupported; } - if (audioStream != null && !audioSupported) + if (!audioSupported) { reasons |= TranscodeReason.AudioCodecNotSupported; } @@ -587,21 +586,19 @@ private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options) } // Collect candidate audio streams - IEnumerable candidateAudioStreams = audioStream == null ? Array.Empty() : new[] { audioStream }; + ICollection candidateAudioStreams = audioStream == null ? Array.Empty() : new[] { audioStream }; if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0) { if (audioStream?.IsDefault == true) { - candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault); + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray(); } else { - candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language); + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language).ToArray(); } } - candidateAudioStreams = candidateAudioStreams.ToArray(); - var videoStream = item.VideoStream; var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay); @@ -1057,7 +1054,7 @@ private static int GetMaxAudioBitrateForTotalBitrate(long totalBitrate) MediaSourceInfo mediaSource, MediaStream videoStream, MediaStream audioStream, - IEnumerable candidateAudioStreams, + ICollection candidateAudioStreams, MediaStream subtitleStream, bool isEligibleForDirectPlay, bool isEligibleForDirectStream) @@ -1179,14 +1176,18 @@ private static int GetMaxAudioBitrateForTotalBitrate(long totalBitrate) } // Check audio codec - var selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec)); - if (selectedAudioStream == null) - { - directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported; - } - else + MediaStream selectedAudioStream = null; + if (candidateAudioStreams.Any()) { - audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream); + selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec)); + if (selectedAudioStream == null) + { + directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported; + } + else + { + audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream); + } } var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons; diff --git a/README.md b/README.md index e81ea64c06..d225e241e2 100644 --- a/README.md +++ b/README.md @@ -1,176 +1,33 @@ -

Jellyfin

+

Veso

The Free Software Media System

- ---- +

A Jellyfin Fork with Fixes and Usability in Mind

-Logo Banner -
-
- -GPL 2.0 License - - -Current Release - - -Translation Status - - -Azure Builds - - -Docker Pull Count - -
- -Donate - - -Submit Feature Requests - - -Chat on Matrix - - -Join our Subreddit - - -Release RSS Feed - - -Master Commits RSS Feed - +Logo Banner

---- - -Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest! - -For further details, please see [our documentation page](https://docs.jellyfin.org/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels](https://docs.jellyfin.org/general/getting-help.html). For more information about the project, please see our [about page](https://docs.jellyfin.org/general/about.html). - -Want to get started?
-Check out our downloads page or our installation guide, then see our quick start guide. You can also build from source.
- -Something not working right?
-Open an Issue on GitHub.
- -Want to contribute?
-Check out our contributing choose-your-own-adventure to see where you can help, then see our contributing guide and our community standards.
- -New idea or improvement?
-Check out our feature request hub.
- -Don't see Jellyfin in your language?
-Check out our Weblate instance to help translate Jellyfin and its subprojects.
- - -Detailed Translation Status - - ---- - -## Jellyfin Server - -This repository contains the code for Jellyfin's backend server. Note that this is only one of many projects under the Jellyfin GitHub [organization](https://github.com/jellyfin/) on GitHub. If you want to contribute, you can start by checking out our [documentation](https://jellyfin.org/docs/general/contributing/index.html) to see what to work on. - -## Server Development - -These instructions will help you get set up with a local development environment in order to contribute to this repository. Before you start, please be sure to completely read our [guidelines on development contributions](https://jellyfin.org/docs/general/contributing/development.html). Note that this project is supported on all major operating systems except FreeBSD, which is still incompatible. +### docker-compose -### Prerequisites - -Before the project can be built, you must first install the [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system. - -Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET 6 development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2022) and [Visual Studio Code](https://code.visualstudio.com/Download). - -[ffmpeg](https://github.com/jellyfin/jellyfin-ffmpeg) will also need to be installed. - -### Cloning the Repository - -After dependencies are installed you will need to clone a local copy of this repository. If you just want to run the server from source you can clone this repository directly, but if you are intending to contribute code changes to the project, you should [set up your own fork](https://jellyfin.org/docs/general/contributing/development.html#set-up-your-copy-of-the-repo) of the repository. The following example shows how you can clone the repository directly over HTTPS. - -```bash -git clone https://github.com/jellyfin/jellyfin.git ``` +version: '3.7' +services: + veso: + container_name: veso + environment: + - TZ=America/New_York + user: 1000:1000 + volumes: + - '/dev/dri:/dev/dri' + - '/path/to/config:/config' + - '/path/to/cache:/cache' + - '/path/to/media:/media' + ports: + - 8096:8096 + - 8920:8920 + devices: + - /dev/dri:/dev/dri + restart: unless-stopped + image: vesotv/veso:latest -### Installing the Web Client - -The server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend by default. Before you can run the server, you will need to get a copy of the web client since they are not included in this repository directly. - -Note that it is also possible to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step. - -There are three options to get the files for the web client. - -1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27&_a=summary&repositoryFilter=6&view=branches) of the pipelines page. -2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) -3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web` - -### Running The Server - -The following instructions will help you get the project up and running via the command line, or your preferred IDE. - -#### Running With Visual Studio - -To run the project with Visual Studio you can open the Solution (`.sln`) file and then press `F5` to run the server. - -#### Running With Visual Studio Code - -To run the project with Visual Studio Code you will first need to open the repository directory with Visual Studio Code using the `Open Folder...` option. - -Second, you need to [install the recommended extensions for the workspace](https://code.visualstudio.com/docs/editor/extension-gallery#_recommended-extensions). Note that extension recommendations are classified as either "Workspace Recommendations" or "Other Recommendations", but only the "Workspace Recommendations" are required. - -After the required extensions are installed, you can run the server by pressing `F5`. - -#### Running From The Command Line - -To run the server from the command line you can use the `dotnet run` command. The example below shows how to do this if you have cloned the repository into a directory named `jellyfin` (the default directory name) and should work on all operating systems. - -```bash -cd jellyfin # Move into the repository directory -dotnet run --project Jellyfin.Server --webdir /absolute/path/to/jellyfin-web/dist # Run the server startup project -``` - -A second option is to build the project and then run the resulting executable file directly. When running the executable directly you can easily add command line options. Add the `--help` flag to list details on all the supported command line options. - -1. Build the project - -```bash -dotnet build # Build the project -cd Jellyfin.Server/bin/Debug/net6.0 # Change into the build output directory ``` - -2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`. - -### Running The Tests - -This repository also includes unit tests that are used to validate functionality as part of a CI pipeline on Azure. There are several ways to run these tests. - -1. Run tests from the command line using `dotnet test` -2. Run tests in Visual Studio using the [Test Explorer](https://docs.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer) -3. Run individual tests in Visual Studio Code using the associated [CodeLens annotation](https://github.com/OmniSharp/omnisharp-vscode/wiki/How-to-run-and-debug-unit-tests) - -### Advanced Configuration - -The following sections describe some more advanced scenarios for running the server from source that build upon the standard instructions above. - -#### Hosting The Web Client Separately - -It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this. - -To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can specified using the command line -switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`. - -Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar. - -**NOTE:** The setup wizard can not be run if the web client is hosted separately. - ---- -

-This project is supported by: -
-
-DigitalOcean -   -JetBrains logo -

+Join us on discord https://discord.gg/Ce4PmFcX7Y diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs index fe0d7fc90f..6d90929e1f 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs @@ -14,7 +14,7 @@ public void Parse_Valid_Success() { using (var stream = File.OpenRead("Test Data/example.ass")) { - var parsed = new SubtitleEditParser(new NullLogger()).Parse(stream, "ass"); + var parsed = new AssParser(new NullLogger()).Parse(stream, CancellationToken.None); Assert.Single(parsed.TrackEvents); var trackEvent = parsed.TrackEvents[0]; diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs index 2aebee5562..a1e9414461 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs @@ -14,7 +14,7 @@ public void Parse_Valid_Success() { using (var stream = File.OpenRead("Test Data/example.srt")) { - var parsed = new SubtitleEditParser(new NullLogger()).Parse(stream, "srt"); + var parsed = new SrtParser(new NullLogger()).Parse(stream, CancellationToken.None); Assert.Equal(2, parsed.TrackEvents.Count); var trackEvent1 = parsed.TrackEvents[0]; @@ -36,7 +36,7 @@ public void Parse_EmptyNewlineBetweenText_Success() { using (var stream = File.OpenRead("Test Data/example2.srt")) { - var parsed = new SubtitleEditParser(new NullLogger()).Parse(stream, "srt"); + var parsed = new SrtParser(new NullLogger()).Parse(stream, CancellationToken.None); Assert.Equal(2, parsed.TrackEvents.Count); var trackEvent1 = parsed.TrackEvents[0]; diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs index 6abf2d26cb..e5451d4621 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs @@ -12,7 +12,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { public class SsaParserTests { - private readonly SubtitleEditParser _parser = new SubtitleEditParser(new NullLogger()); + private readonly SsaParser _parser = new SsaParser(new NullLogger()); [Theory] [MemberData(nameof(Parse_MultipleDialogues_TestData))] @@ -20,7 +20,7 @@ public void Parse_MultipleDialogues_Success(string ssa, IReadOnlyList