diff --git a/.ci/azure-pipelines-compat.yml b/.ci/azure-pipelines-compat.yml index de60d2ccb1..1ffaaf2b98 100644 --- a/.ci/azure-pipelines-compat.yml +++ b/.ci/azure-pipelines-compat.yml @@ -1,13 +1,13 @@ parameters: - - name: Packages - type: object - default: {} - - name: LinuxImage - type: string - default: "ubuntu-latest" - - name: DotNetSdkVersion - type: string - default: 3.1.100 +- name: Packages + type: object + default: {} +- name: LinuxImage + type: string + default: "ubuntu-latest" +- name: DotNetSdkVersion + type: string + default: 3.1.100 jobs: - job: CompatibilityCheck diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index d155624abb..456be7108f 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -20,41 +20,34 @@ jobs: submodules: true persistCredentials: true - - task: CmdLine@2 - displayName: "Clone Web Branch" - condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')) + - task: DownloadPipelineArtifact@2 + displayName: "Download Web Branch" + condition: in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion') inputs: - script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" + path: '$(Agent.TempDirectory)' + artifact: 'jellyfin-web-production' + source: 'specific' + project: 'jellyfin' + pipeline: 'Jellyfin Web' + runBranch: variables['Build.SourceBranch'] - - task: CmdLine@2 - displayName: "Clone Web Target" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest')) + - task: DownloadPipelineArtifact@2 + displayName: "Download Web Target" + condition: eq(variables['Build.Reason'], 'PullRequest') inputs: - script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" + path: '$(Agent.TempDirectory)' + artifact: 'jellyfin-web-production' + source: 'specific' + project: 'jellyfin' + pipeline: 'Jellyfin Web' + runBranch: variables['System.PullRequest.TargetBranch'] - - task: NodeTool@0 - displayName: "Install Node" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) + - task: ExtractFiles@1 + displayName: "Extract Web Client" inputs: - versionSpec: "12.x" - - - task: CmdLine@2 - displayName: "Build Web Client" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - script: yarn install - workingDirectory: $(Agent.TempDirectory)/jellyfin-web - - - task: CopyFiles@2 - displayName: "Copy Web Client" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist - contents: "**" - targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web - cleanTargetFolder: true - overWrite: true - flattenFolders: false + archiveFilePatterns: '$(Agent.TempDirectory)/*.zip' + destinationFolder: '$(Build.SourcesDirectory)/MediaBrowser.WebDashboard' + cleanDestinationFolder: false - task: UseDotNet@2 displayName: "Update DotNet" diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index 4455632e15..cb5338ac8c 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -1,26 +1,25 @@ parameters: - - name: ImageNames - type: object - default: - Linux: "ubuntu-latest" - Windows: "windows-latest" - macOS: "macos-latest" - - name: TestProjects - type: string - default: "tests/**/*Tests.csproj" - - name: DotNetSdkVersion - type: string - default: 3.1.100 +- name: ImageNames + type: object + default: + Linux: "ubuntu-latest" + Windows: "windows-latest" + macOS: "macos-latest" +- name: TestProjects + type: string + default: "tests/**/*Tests.csproj" +- name: DotNetSdkVersion + type: string + default: 3.1.100 jobs: - - job: MainTest - displayName: Main Test + - job: Test + displayName: Test strategy: matrix: ${{ each imageName in parameters.ImageNames }}: ${{ imageName.key }}: ImageName: ${{ imageName.value }} - maxParallel: 3 pool: vmImage: "$(ImageName)" steps: @@ -29,14 +28,30 @@ jobs: submodules: true persistCredentials: false + # This is required for the SonarCloud analyzer + - task: UseDotNet@2 + displayName: "Install .NET Core SDK 2.1" + condition: eq(variables['ImageName'], 'ubuntu-latest') + inputs: + packageType: sdk + version: '2.1.805' + - task: UseDotNet@2 displayName: "Update DotNet" inputs: packageType: sdk version: ${{ parameters.DotNetSdkVersion }} + - task: SonarCloudPrepare@1 + displayName: 'Prepare analysis on SonarCloud' + condition: eq(variables['ImageName'], 'ubuntu-latest') + inputs: + SonarCloud: 'Sonarcloud for Jellyfin' + organization: 'jellyfin' + projectKey: 'jellyfin_jellyfin' + - task: DotNetCoreCLI@2 - displayName: Run .NET Core CLI tests + displayName: 'Run CLI Tests' inputs: command: "test" projects: ${{ parameters.TestProjects }} @@ -45,9 +60,17 @@ jobs: testRunTitle: $(Agent.JobName) workingDirectory: "$(Build.SourcesDirectory)" + - task: SonarCloudAnalyze@1 + displayName: 'Run Code Analysis' + condition: eq(variables['ImageName'], 'ubuntu-latest') + + - task: SonarCloudPublish@1 + displayName: 'Publish Quality Gate Result' + condition: eq(variables['ImageName'], 'ubuntu-latest') + - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging - displayName: ReportGenerator (merge) + displayName: 'Run ReportGenerator' inputs: reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml" targetdir: "$(Agent.TempDirectory)/merged/" @@ -56,10 +79,11 @@ jobs: ## V2 is already in the repository but it does not work "wrong number of segments" YAML error. - task: PublishCodeCoverageResults@1 condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging - displayName: Publish Code Coverage + displayName: 'Publish Code Coverage' inputs: codeCoverageTool: "cobertura" #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2 summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml" pathToSources: $(Build.SourcesDirectory) failIfCoverageEmpty: true + diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 3522cbf006..1a439c7185 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -1,12 +1,12 @@ name: $(Date:yyyyMMdd)$(Rev:.r) variables: - - name: TestProjects - value: "tests/**/*Tests.csproj" - - name: RestoreBuildProjects - value: "Jellyfin.Server/Jellyfin.Server.csproj" - - name: DotNetSdkVersion - value: 3.1.100 +- name: TestProjects + value: "tests/**/*Tests.csproj" +- name: RestoreBuildProjects + value: "Jellyfin.Server/Jellyfin.Server.csproj" +- name: DotNetSdkVersion + value: 3.1.100 pr: autoCancel: true diff --git a/.copr/Makefile b/.copr/Makefile deleted file mode 100644 index ba330ada95..0000000000 --- a/.copr/Makefile +++ /dev/null @@ -1,59 +0,0 @@ -VERSION := $(shell sed -ne '/^Version:/s/.* *//p' \ - deployment/fedora-package-x64/pkg-src/jellyfin.spec) - -deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz: - curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \ - https://github.com/jellyfin/jellyfin-web/archive/v$(VERSION).tar.gz \ - || curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \ - https://github.com/jellyfin/jellyfin-web/archive/master.tar.gz \ - -srpm: deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz - cd deployment/fedora-package-x64; \ - SOURCE_DIR=../.. \ - WORKDIR="$${PWD}"; \ - package_temporary_dir="$${WORKDIR}/pkg-dist-tmp"; \ - pkg_src_dir="$${WORKDIR}/pkg-src"; \ - GNU_TAR=1; \ - tar \ - --transform "s,^\.,jellyfin-$(VERSION)," \ - --exclude='.git*' \ - --exclude='**/.git' \ - --exclude='**/.hg' \ - --exclude='**/.vs' \ - --exclude='**/.vscode' \ - --exclude='deployment' \ - --exclude='**/bin' \ - --exclude='**/obj' \ - --exclude='**/.nuget' \ - --exclude='*.deb' \ - --exclude='*.rpm' \ - -czf "pkg-src/jellyfin-$(VERSION).tar.gz" \ - -C $${SOURCE_DIR} ./ || GNU_TAR=0; \ - if [ $${GNU_TAR} -eq 0 ]; then \ - package_temporary_dir="$$(mktemp -d)"; \ - mkdir -p "$${package_temporary_dir}/jellyfin"; \ - tar \ - --exclude='.git*' \ - --exclude='**/.git' \ - --exclude='**/.hg' \ - --exclude='**/.vs' \ - --exclude='**/.vscode' \ - --exclude='deployment' \ - --exclude='**/bin' \ - --exclude='**/obj' \ - --exclude='**/.nuget' \ - --exclude='*.deb' \ - --exclude='*.rpm' \ - -czf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz" \ - -C $${SOURCE_DIR} ./; \ - mkdir -p "$${package_temporary_dir}/jellyfin-$(VERSION)"; \ - tar -xzf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz" \ - -C "$${package_temporary_dir}/jellyfin-$(VERSION); \ - rm -f "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"; \ - tar -czf "$${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-$(VERSION).tar.gz" \ - -C "$${package_temporary_dir}" "jellyfin-$(VERSION); \ - rm -rf $${package_temporary_dir}; \ - fi; \ - rpmbuild -bs pkg-src/jellyfin.spec \ - --define "_sourcedir $$PWD/pkg-src/" \ - --define "_srcrpmdir $(outdir)" diff --git a/.copr/Makefile b/.copr/Makefile new file mode 120000 index 0000000000..ec3c90dfd9 --- /dev/null +++ b/.copr/Makefile @@ -0,0 +1 @@ +../fedora/Makefile \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index dc9aaa3ed5..b84e563efa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,7 +13,7 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true end_of_line = lf -max_line_length = null +max_line_length = off # YAML indentation [*.{yml,yaml}] @@ -22,6 +22,7 @@ indent_size = 2 # XML indentation [*.{csproj,xml}] indent_size = 2 + ############################### # .NET Coding Conventions # ############################### @@ -51,11 +52,12 @@ dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent -dotnet_prefer_inferred_tuple_names = true:suggestion -dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_auto_properties = true:silent dotnet_style_prefer_conditional_expression_over_assignment = true:silent dotnet_style_prefer_conditional_expression_over_return = true:silent + ############################### # Naming Conventions # ############################### @@ -67,7 +69,7 @@ dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field -dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected dotnet_naming_symbols.non_private_static_fields.required_modifiers = static dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case @@ -159,6 +161,7 @@ csharp_style_deconstructed_variable_declaration = true:suggestion csharp_prefer_simple_default_expression = true:suggestion csharp_style_pattern_local_over_anonymous_function = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion + ############################### # C# Formatting Rules # ############################### @@ -189,9 +192,3 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true -############################### -# VB Coding Conventions # -############################### -[*.vb] -# Modifier preferences -visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..e902dc7124 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Joshua must review all changes to deployment and build.sh +deployment/* @joshuaboniface +build.sh @joshuaboniface diff --git a/.gitignore b/.gitignore index 523c45a7e6..46f036ad91 100644 --- a/.gitignore +++ b/.gitignore @@ -245,14 +245,14 @@ pip-log.txt ######################### # Artifacts for debian-x64 -deployment/debian-package-x64/pkg-src/.debhelper/ -deployment/debian-package-x64/pkg-src/*.debhelper -deployment/debian-package-x64/pkg-src/debhelper-build-stamp -deployment/debian-package-x64/pkg-src/files -deployment/debian-package-x64/pkg-src/jellyfin.substvars -deployment/debian-package-x64/pkg-src/jellyfin/ +debian/.debhelper/ +debian/*.debhelper +debian/debhelper-build-stamp +debian/files +debian/jellyfin.substvars +debian/jellyfin/ # Don't ignore the debian/bin folder -!deployment/debian-package-x64/pkg-src/bin/ +!debian/bin/ deployment/**/dist/ deployment/**/pkg-dist/ @@ -272,3 +272,8 @@ dist # BenchmarkDotNet artifacts BenchmarkDotNet.Artifacts + +# Ignore web artifacts from native builds +web/ +web-src.* +MediaBrowser.WebDashboard/jellyfin-web/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..59d9452fed --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "ms-dotnettools.csharp", + "editorconfig.editorconfig" + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [ + + ] +} diff --git a/Dockerfile b/Dockerfile index caac7500a5..d3fb138a81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,8 @@ ARG DOTNET_VERSION=3.1 -ARG FFMPEG_VERSION=latest FROM node: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 \ +RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && yarn install \ @@ -17,7 +16,6 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" -FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg FROM debian:buster-slim # https://askubuntu.com/questions/972516/debian-frontend-environment-variable @@ -27,32 +25,26 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" -COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web # Install dependencies: -# libfontconfig1: needed for Skia -# libgomp1: needed for ffmpeg -# libva-drm2: needed for ffmpeg -# mesa-va-drivers: needed for VAAPI +# mesa-va-drivers: needed for AMD VAAPI RUN apt-get update \ + && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \ + && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \ + && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \ + && apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y \ - libfontconfig1 \ - libgomp1 \ - libva-drm2 \ mesa-va-drivers \ + jellyfin-ffmpeg \ openssl \ - ca-certificates \ - vainfo \ - i965-va-driver \ locales \ - && apt-get clean autoclean -y\ - && apt-get autoremove -y\ + && apt-get remove gnupg wget apt-transport-https -y \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media \ - && ln -s /opt/ffmpeg/bin/ffmpeg /usr/local/bin \ - && ln -s /opt/ffmpeg/bin/ffprobe /usr/local/bin \ && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 @@ -65,4 +57,4 @@ VOLUME /cache /config /media ENTRYPOINT ["./jellyfin/jellyfin", \ "--datadir", "/config", \ "--cachedir", "/cache", \ - "--ffmpeg", "/usr/local/bin/ffmpeg"] + "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] diff --git a/Dockerfile.arm b/Dockerfile.arm index 39beaa4791..59b8a8c982 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -38,7 +38,7 @@ COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \ curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \ - curl -s https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \ + curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \ echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \ echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \ apt-get update && \ diff --git a/DvdLib/BigEndianBinaryReader.cs b/DvdLib/BigEndianBinaryReader.cs index b3b2eabd5c..b3aad85cec 100644 --- a/DvdLib/BigEndianBinaryReader.cs +++ b/DvdLib/BigEndianBinaryReader.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CS1591 + +using System.Buffers.Binary; using System.IO; namespace DvdLib @@ -12,19 +14,12 @@ namespace DvdLib public override ushort ReadUInt16() { - return BitConverter.ToUInt16(ReadAndReverseBytes(2), 0); + return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2)); } public override uint ReadUInt32() { - return BitConverter.ToUInt32(ReadAndReverseBytes(4), 0); - } - - private byte[] ReadAndReverseBytes(int count) - { - byte[] val = base.ReadBytes(count); - Array.Reverse(val, 0, count); - return val; + return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4)); } } } diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj index f4df6a9f52..64d041cb05 100644 --- a/DvdLib/DvdLib.csproj +++ b/DvdLib/DvdLib.csproj @@ -1,17 +1,19 @@ - - - + + + {713F42B5-878E-499D-A878-E4C652B1D5E8} + - + netstandard2.1 false true + true diff --git a/DvdLib/Ifo/Cell.cs b/DvdLib/Ifo/Cell.cs index 268ab897ee..2eab400f7d 100644 --- a/DvdLib/Ifo/Cell.cs +++ b/DvdLib/Ifo/Cell.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; namespace DvdLib.Ifo diff --git a/DvdLib/Ifo/CellPlaybackInfo.cs b/DvdLib/Ifo/CellPlaybackInfo.cs index e588e51ac0..6e33a0ec5a 100644 --- a/DvdLib/Ifo/CellPlaybackInfo.cs +++ b/DvdLib/Ifo/CellPlaybackInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; namespace DvdLib.Ifo diff --git a/DvdLib/Ifo/CellPositionInfo.cs b/DvdLib/Ifo/CellPositionInfo.cs index 2b973e0830..216aa0f77a 100644 --- a/DvdLib/Ifo/CellPositionInfo.cs +++ b/DvdLib/Ifo/CellPositionInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; namespace DvdLib.Ifo diff --git a/DvdLib/Ifo/Chapter.cs b/DvdLib/Ifo/Chapter.cs index bd3bd97040..1e69429f82 100644 --- a/DvdLib/Ifo/Chapter.cs +++ b/DvdLib/Ifo/Chapter.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace DvdLib.Ifo { public class Chapter diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs index c0f9cf4107..ca20baa73f 100644 --- a/DvdLib/Ifo/Dvd.cs +++ b/DvdLib/Ifo/Dvd.cs @@ -1,8 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; using System.Linq; -using MediaBrowser.Model.IO; namespace DvdLib.Ifo { @@ -13,13 +14,10 @@ namespace DvdLib.Ifo private ushort _titleCount; public readonly Dictionary VTSPaths = new Dictionary(); - private readonly IFileSystem _fileSystem; - - public Dvd(string path, IFileSystem fileSystem) + public Dvd(string path) { - _fileSystem = fileSystem; Titles = new List(); - var allFiles = _fileSystem.GetFiles(path, true).ToList(); + var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories); var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ?? allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase)); @@ -76,7 +74,7 @@ namespace DvdLib.Ifo } } - private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles) + private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles) { var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum); diff --git a/DvdLib/Ifo/DvdTime.cs b/DvdLib/Ifo/DvdTime.cs index 3688089ec7..978af90c2e 100644 --- a/DvdLib/Ifo/DvdTime.cs +++ b/DvdLib/Ifo/DvdTime.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace DvdLib.Ifo diff --git a/DvdLib/Ifo/Program.cs b/DvdLib/Ifo/Program.cs index af08afa356..9f62512706 100644 --- a/DvdLib/Ifo/Program.cs +++ b/DvdLib/Ifo/Program.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace DvdLib.Ifo diff --git a/DvdLib/Ifo/ProgramChain.cs b/DvdLib/Ifo/ProgramChain.cs index 7b003005b9..4860360afd 100644 --- a/DvdLib/Ifo/ProgramChain.cs +++ b/DvdLib/Ifo/ProgramChain.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/DvdLib/Ifo/Title.cs b/DvdLib/Ifo/Title.cs index 335e929928..abf806d2c0 100644 --- a/DvdLib/Ifo/Title.cs +++ b/DvdLib/Ifo/Title.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.IO; diff --git a/DvdLib/Ifo/UserOperation.cs b/DvdLib/Ifo/UserOperation.cs index 757a5a05db..5d111ebc06 100644 --- a/DvdLib/Ifo/UserOperation.cs +++ b/DvdLib/Ifo/UserOperation.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace DvdLib.Ifo diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs index e224d10bd3..dba9019677 100644 --- a/Emby.Dlna/ConfigurationExtension.cs +++ b/Emby.Dlna/ConfigurationExtension.cs @@ -1,3 +1,4 @@ +#nullable enable #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 88cc00e30a..f7d840c623 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -425,57 +425,99 @@ namespace Emby.Dlna.Didl } } - if (item is Episode episode && context is Season season) + return item is Episode episode + ? GetEpisodeDisplayName(episode, context) + : item.Name; + } + + /// <summary> + /// Gets episode display name appropriate for the given context. + /// </summary> + /// <remarks> + /// If context is a season, this will return a string containing just episode number and name. + /// Otherwise the result will include series nams and season number. + /// </remarks> + /// <param name="episode">The episode.</param> + /// <param name="context">Current context.</param> + /// <returns>Formatted name of the episode.</returns> + private string GetEpisodeDisplayName(Episode episode, BaseItem context) + { + string[] components; + + if (context is Season season) { // This is a special embedded within a season - if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value == 0 + if (episode.ParentIndexNumber.HasValue && episode.ParentIndexNumber.Value == 0 && season.IndexNumber.HasValue && season.IndexNumber.Value != 0) { return string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("ValueSpecialEpisodeName"), - item.Name); + episode.Name); } - if (item.IndexNumber.HasValue) - { - var number = item.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); + // inside a season use simple format (ex. '12 - Episode Name') + var epNumberName = GetEpisodeIndexFullName(episode); + components = new[] { epNumberName, episode.Name }; + } + else + { + // outside a season include series and season details (ex. 'TV Show - S05E11 - Episode Name') + var epNumberName = GetEpisodeNumberDisplayName(episode); + components = new[] { episode.SeriesName, epNumberName, episode.Name }; + } - if (episode.IndexNumberEnd.HasValue) - { - number += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture); - } + return string.Join(" - ", components.Where(NotNullOrWhiteSpace)); + } - return number + " - " + item.Name; - } - } - else if (item is Episode ep) + /// <summary> + /// Gets complete episode number. + /// </summary> + /// <param name="episode">The episode.</param> + /// <returns>For single episodes returns just the number. For double episodes - current and ending numbers.</returns> + private string GetEpisodeIndexFullName(Episode episode) + { + var name = string.Empty; + if (episode.IndexNumber.HasValue) { - var parent = ep.GetParent(); - var name = parent.Name + " - "; + name += episode.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); - if (ep.ParentIndexNumber.HasValue) + if (episode.IndexNumberEnd.HasValue) { - name += "S" + ep.ParentIndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); - } - else if (!item.IndexNumber.HasValue) - { - return name + " - " + item.Name; + name += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture); } + } - name += "E" + ep.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); - if (ep.IndexNumberEnd.HasValue) - { - name += "-" + ep.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture); - } + return name; + } + + /// <summary> + /// Gets episode number formatted as 'S##E##'. + /// </summary> + /// <param name="episode">The episode.</param> + /// <returns>Formatted episode number.</returns> + private string GetEpisodeNumberDisplayName(Episode episode) + { + var name = string.Empty; + var seasonNumber = episode.Season?.IndexNumber; - name += " - " + item.Name; - return name; + if (seasonNumber.HasValue) + { + name = "S" + seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture); + } + + var indexName = GetEpisodeIndexFullName(episode); + + if (!string.IsNullOrWhiteSpace(indexName)) + { + name += "E" + indexName; } - return item.Name; + return name; } + private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s); + private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) { writer.WriteStartElement(string.Empty, "res", NS_DIDL); @@ -1018,19 +1060,58 @@ namespace Emby.Dlna.Didl } } - item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); + // For audio tracks without art use album art if available. + if (item is Audio audioItem) + { + var album = audioItem.AlbumEntity; + return album != null && album.HasImage(ImageType.Primary) + ? GetImageInfo(album, ImageType.Primary) + : null; + } - if (item != null) + // Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder. + if (item is MusicAlbum || item is Playlist) { - if (item.HasImage(ImageType.Primary)) - { - return GetImageInfo(item, ImageType.Primary); - } + return null; + } + + // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item. + var parentWithImage = GetFirstParentWithImageBelowUserRoot(item); + if (parentWithImage != null) + { + return GetImageInfo(parentWithImage, ImageType.Primary); } return null; } + private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item) + { + if (item == null) + { + return null; + } + + if (item.HasImage(ImageType.Primary)) + { + return item; + } + + var parent = item.GetParent(); + if (parent is UserRootFolder) + { + return null; + } + + // terminate in case we went past user root folder (unlikely?) + if (parent is Folder folder && folder.IsRoot) + { + return null; + } + + return GetFirstParentWithImageBelowUserRoot(parent); + } + private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) { var imageInfo = item.GetImageInfo(type, 0); diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 0cabe43d51..42a5f95c14 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid> + </PropertyGroup> + <ItemGroup> <Compile Include="..\SharedVersion.cs" /> </ItemGroup> diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 43e9830540..9d7c0d3659 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -908,7 +908,8 @@ namespace Emby.Dlna.PlayTo return 0; } - public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken) { if (_disposed) { @@ -924,10 +925,12 @@ namespace Emby.Dlna.PlayTo { return SendPlayCommand(data as PlayRequest, cancellationToken); } + if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) { return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); } + if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) { return SendGeneralCommand(data as GeneralCommand, cancellationToken); diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs index d10804b228..2347ebd0d3 100644 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -12,7 +12,7 @@ namespace Emby.Dlna.Profiles { Name = "Generic Device"; - ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*"; + ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*"; Manufacturer = "Jellyfin"; ModelDescription = "UPnP/AV 1.0 Compliant Media Server"; diff --git a/Emby.Dlna/Profiles/Xml/Default.xml b/Emby.Dlna/Profiles/Xml/Default.xml index daac4135a2..9460f9d5a1 100644 --- a/Emby.Dlna/Profiles/Xml/Default.xml +++ b/Emby.Dlna/Profiles/Xml/Default.xml @@ -21,7 +21,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Denon AVR.xml b/Emby.Dlna/Profiles/Xml/Denon AVR.xml index c76cd9a898..571786906c 100644 --- a/Emby.Dlna/Profiles/Xml/Denon AVR.xml +++ b/Emby.Dlna/Profiles/Xml/Denon AVR.xml @@ -26,7 +26,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml b/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml index f2ce68ab5d..eea0febfdc 100644 --- a/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml +++ b/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>10</TimelineOffsetSeconds> <RequiresPlainVideoItems>true</RequiresPlainVideoItems> <RequiresPlainFolders>true</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml index a0f0e0ee8a..20f5ba79bf 100644 --- a/Emby.Dlna/Profiles/Xml/LG Smart TV.xml +++ b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>10</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml b/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml index 55910c77f2..d01e3a145e 100644 --- a/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml +++ b/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml @@ -25,7 +25,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Marantz.xml b/Emby.Dlna/Profiles/Xml/Marantz.xml index a6345ab3f3..0cc9c86e87 100644 --- a/Emby.Dlna/Profiles/Xml/Marantz.xml +++ b/Emby.Dlna/Profiles/Xml/Marantz.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/MediaMonkey.xml b/Emby.Dlna/Profiles/Xml/MediaMonkey.xml index 2c2c3a1de4..9d5ddc3d1a 100644 --- a/Emby.Dlna/Profiles/Xml/MediaMonkey.xml +++ b/Emby.Dlna/Profiles/Xml/MediaMonkey.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml b/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml index 934f0550d3..8f766853bb 100644 --- a/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml +++ b/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml @@ -28,7 +28,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>10</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml b/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml index eab220fae9..aa881d0147 100644 --- a/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml +++ b/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml @@ -21,7 +21,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml b/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml index 3e6f56e5bb..7160a9c2eb 100644 --- a/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml +++ b/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml b/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml index 74240b8435..c9b907e580 100644 --- a/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml +++ b/Emby.Dlna/Profiles/Xml/Sharp Smart TV.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>true</RequiresPlainVideoItems> <RequiresPlainFolders>true</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml index 49819ccfdb..e516ff512d 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml index aaad7b342c..88bd1c2f53 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml index 8e2e8dbaa4..3ca9893cdc 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml index 17a6135e1f..8804a75dfa 100644 --- a/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml index df385135cd..bafa44b828 100644 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml index 20f50f6b63..eb8e645b31 100644 --- a/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml @@ -29,7 +29,7 @@ <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <SonyAggregationFlags>10</SonyAggregationFlags> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/WDTV Live.xml b/Emby.Dlna/Profiles/Xml/WDTV Live.xml index 05380e33a6..ccb74ee646 100644 --- a/Emby.Dlna/Profiles/Xml/WDTV Live.xml +++ b/Emby.Dlna/Profiles/Xml/WDTV Live.xml @@ -28,7 +28,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>5</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/Xbox One.xml b/Emby.Dlna/Profiles/Xml/Xbox One.xml index 5f5cf1af31..26a65bbd44 100644 --- a/Emby.Dlna/Profiles/Xml/Xbox One.xml +++ b/Emby.Dlna/Profiles/Xml/Xbox One.xml @@ -28,7 +28,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>40</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Dlna/Profiles/Xml/foobar2000.xml b/Emby.Dlna/Profiles/Xml/foobar2000.xml index f3eedb35c6..5ce75ace55 100644 --- a/Emby.Dlna/Profiles/Xml/foobar2000.xml +++ b/Emby.Dlna/Profiles/Xml/foobar2000.xml @@ -27,7 +27,7 @@ <MaxStaticBitrate>140000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> - <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*</ProtocolInfo> + <ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo> <TimelineOffsetSeconds>0</TimelineOffsetSeconds> <RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainFolders>false</RequiresPlainFolders> diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index b7090b2629..092f8580a6 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -1,10 +1,16 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{08FFF49B-F175-4807-A2B5-73B0EBD9F716}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index eca4b56eb9..0b3bbe29ef 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -8,7 +8,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; @@ -33,8 +32,7 @@ namespace Emby.Drawing private readonly IFileSystem _fileSystem; private readonly IServerApplicationPaths _appPaths; private readonly IImageEncoder _imageEncoder; - private readonly Func<ILibraryManager> _libraryManager; - private readonly Func<IMediaEncoder> _mediaEncoder; + private readonly IMediaEncoder _mediaEncoder; private bool _disposed = false; @@ -45,20 +43,17 @@ namespace Emby.Drawing /// <param name="appPaths">The server application paths.</param> /// <param name="fileSystem">The filesystem.</param> /// <param name="imageEncoder">The image encoder.</param> - /// <param name="libraryManager">The library manager.</param> /// <param name="mediaEncoder">The media encoder.</param> public ImageProcessor( ILogger<ImageProcessor> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IImageEncoder imageEncoder, - Func<ILibraryManager> libraryManager, - Func<IMediaEncoder> mediaEncoder) + IMediaEncoder mediaEncoder) { _logger = logger; _fileSystem = fileSystem; _imageEncoder = imageEncoder; - _libraryManager = libraryManager; _mediaEncoder = mediaEncoder; _appPaths = appPaths; } @@ -121,26 +116,9 @@ namespace Emby.Drawing /// <inheritdoc /> public async Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options) { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - var libraryManager = _libraryManager(); - ItemImageInfo originalImage = options.Image; BaseItem item = options.Item; - if (!originalImage.IsLocalFile) - { - if (item == null) - { - item = libraryManager.GetItemById(options.ItemId); - } - - originalImage = await libraryManager.ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false); - } - string originalImagePath = originalImage.Path; DateTime dateModified = originalImage.DateModified; ImageDimensions? originalImageSize = null; @@ -312,10 +290,6 @@ namespace Emby.Drawing /// <inheritdoc /> public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info) - => GetImageDimensions(item, info, true); - - /// <inheritdoc /> - public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem) { int width = info.Width; int height = info.Height; @@ -332,11 +306,6 @@ namespace Emby.Drawing info.Width = size.Width; info.Height = size.Height; - if (updateItem) - { - _libraryManager().UpdateImages(item); - } - return size; } @@ -351,19 +320,12 @@ namespace Emby.Drawing /// <inheritdoc /> public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) { - try + return GetImageCacheTag(item, new ItemImageInfo { - return GetImageCacheTag(item, new ItemImageInfo - { - Path = chapter.ImagePath, - Type = ImageType.Chapter, - DateModified = chapter.ImageDateModified - }); - } - catch - { - return null; - } + Path = chapter.ImagePath, + Type = ImageType.Chapter, + DateModified = chapter.ImageDateModified + }); } private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) @@ -384,13 +346,13 @@ namespace Emby.Drawing { string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); - string cacheExtension = _mediaEncoder().SupportsEncoder("libwebp") ? ".webp" : ".png"; + string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); var file = _fileSystem.GetFileInfo(outputPath); if (!file.Exists) { - await _mediaEncoder().ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); + await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); } else diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs index 33f4468d9f..b63be3a647 100644 --- a/Emby.Naming/Audio/AlbumParser.cs +++ b/Emby.Naming/Audio/AlbumParser.cs @@ -1,9 +1,9 @@ +#nullable enable #pragma warning disable CS1591 using System; using System.Globalization; using System.IO; -using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Common; @@ -21,8 +21,7 @@ namespace Emby.Naming.Audio public bool IsMultiPart(string path) { var filename = Path.GetFileName(path); - - if (string.IsNullOrEmpty(filename)) + if (filename.Length == 0) { return false; } @@ -39,18 +38,22 @@ namespace Emby.Naming.Audio filename = filename.Replace(')', ' '); filename = Regex.Replace(filename, @"\s+", " "); - filename = filename.TrimStart(); + ReadOnlySpan<char> trimmedFilename = filename.TrimStart(); foreach (var prefix in _options.AlbumStackingPrefixes) { - if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) != 0) + if (!trimmedFilename.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { continue; } - var tmp = filename.Substring(prefix.Length); + var tmp = trimmedFilename.Slice(prefix.Length).Trim(); - tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty; + int index = tmp.IndexOf(' '); + if (index != -1) + { + tmp = tmp.Slice(0, index); + } if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) { diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs index 25d5f8735e..6b2f4be93e 100644 --- a/Emby.Naming/Audio/AudioFileParser.cs +++ b/Emby.Naming/Audio/AudioFileParser.cs @@ -1,3 +1,4 @@ +#nullable enable #pragma warning disable CS1591 using System; @@ -11,7 +12,7 @@ namespace Emby.Naming.Audio { public static bool IsAudioFile(string path, NamingOptions options) { - var extension = Path.GetExtension(path) ?? string.Empty; + var extension = Path.GetExtension(path); return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); } } diff --git a/Emby.Naming/Common/EpisodeExpression.cs b/Emby.Naming/Common/EpisodeExpression.cs index 07de728514..ed6ba8881c 100644 --- a/Emby.Naming/Common/EpisodeExpression.cs +++ b/Emby.Naming/Common/EpisodeExpression.cs @@ -23,11 +23,6 @@ namespace Emby.Naming.Common { } - public EpisodeExpression() - : this(null) - { - } - public string Expression { get => _expression; @@ -48,6 +43,6 @@ namespace Emby.Naming.Common public string[] DateTimeFormats { get; set; } - public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled)); + public Regex Regex => _regex ??= new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled); } } diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 4e08170a47..c017e76c74 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs index 88ec3e2d60..24e59f90a3 100644 --- a/Emby.Naming/Subtitles/SubtitleParser.cs +++ b/Emby.Naming/Subtitles/SubtitleParser.cs @@ -1,3 +1,4 @@ +#nullable enable #pragma warning disable CS1591 using System; @@ -16,11 +17,11 @@ namespace Emby.Naming.Subtitles _options = options; } - public SubtitleInfo ParseFile(string path) + public SubtitleInfo? ParseFile(string path) { - if (string.IsNullOrEmpty(path)) + if (path.Length == 0) { - throw new ArgumentNullException(nameof(path)); + throw new ArgumentException("File path can't be empty.", nameof(path)); } var extension = Path.GetExtension(path); @@ -52,11 +53,6 @@ namespace Emby.Naming.Subtitles private string[] GetFlags(string path) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _. var file = Path.GetFileName(path); diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 7f755fd25e..948fe037b5 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -227,7 +227,7 @@ namespace Emby.Naming.Video } return remainingFiles - .Where(i => i.ExtraType == null) + .Where(i => i.ExtraType != null) .Where(i => baseNames.Any(b => i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) .ToList(); diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index 0b75a8cce9..b4aee614b0 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -89,14 +89,14 @@ namespace Emby.Naming.Video if (parseName) { var cleanDateTimeResult = CleanDateTime(name); + name = cleanDateTimeResult.Name; + year = cleanDateTimeResult.Year; if (extraResult.ExtraType == null - && TryCleanString(cleanDateTimeResult.Name, out ReadOnlySpan<char> newName)) + && TryCleanString(name, out ReadOnlySpan<char> newName)) { name = newName.ToString(); } - - year = cleanDateTimeResult.Year; } return new VideoFileInfo diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index e6bf785bff..1d430a5e58 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{2E030C33-6923-4530-9E54-FA29FA6AD1A9}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs index befecc570b..869b7407e2 100644 --- a/Emby.Notifications/NotificationEntryPoint.cs +++ b/Emby.Notifications/NotificationEntryPoint.cs @@ -143,7 +143,7 @@ namespace Emby.Notifications var notification = new NotificationRequest { - Description = "Please see jellyfin.media for details.", + Description = "Please see jellyfin.org for details.", NotificationType = type, Name = _localization.GetLocalizedString("NewVersionIsAvailable") }; diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index cc3fbb43f9..dbe01257f4 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -1,4 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{89AB4548-770D-41FD-A891-8DAFF44F452C}</ProjectGuid> + </PropertyGroup> + <ItemGroup> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index 63631e512b..987cb7fb2a 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -160,7 +160,7 @@ namespace Emby.Photos try { - var size = _imageProcessor.GetImageDimensions(item, img, false); + var size = _imageProcessor.GetImageDimensions(item, img); if (size.Width > 0 && size.Height > 0) { diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index d900520b2a..3983824a3e 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -1,16 +1,13 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; @@ -27,9 +24,12 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Activity { + /// <summary> + /// Entry point for the activity logger. + /// </summary> public sealed class ActivityLogEntryPoint : IServerEntryPoint { - private readonly ILogger _logger; + private readonly ILogger<ActivityLogEntryPoint> _logger; private readonly IInstallationManager _installationManager; private readonly ISessionManager _sessionManager; private readonly ITaskManager _taskManager; @@ -37,25 +37,21 @@ namespace Emby.Server.Implementations.Activity private readonly ILocalizationManager _localization; private readonly ISubtitleManager _subManager; private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; /// <summary> /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class. /// </summary> - /// <param name="logger"></param> - /// <param name="sessionManager"></param> - /// <param name="deviceManager"></param> - /// <param name="taskManager"></param> - /// <param name="activityManager"></param> - /// <param name="localization"></param> - /// <param name="installationManager"></param> - /// <param name="subManager"></param> - /// <param name="userManager"></param> - /// <param name="appHost"></param> + /// <param name="logger">The logger.</param> + /// <param name="sessionManager">The session manager.</param> + /// <param name="taskManager">The task manager.</param> + /// <param name="activityManager">The activity manager.</param> + /// <param name="localization">The localization manager.</param> + /// <param name="installationManager">The installation manager.</param> + /// <param name="subManager">The subtitle manager.</param> + /// <param name="userManager">The user manager.</param> public ActivityLogEntryPoint( ILogger<ActivityLogEntryPoint> logger, ISessionManager sessionManager, - IDeviceManager deviceManager, ITaskManager taskManager, IActivityManager activityManager, ILocalizationManager localization, @@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity { _logger = logger; _sessionManager = sessionManager; - _deviceManager = deviceManager; _taskManager = taskManager; _activityManager = activityManager; _localization = localization; @@ -74,6 +69,7 @@ namespace Emby.Server.Implementations.Activity _userManager = userManager; } + /// <inheritdoc /> public Task RunAsync() { _taskManager.TaskCompleted += OnTaskCompleted; @@ -98,52 +94,38 @@ namespace Emby.Server.Implementations.Activity _userManager.UserPolicyUpdated += OnUserPolicyUpdated; _userManager.UserLockedOut += OnUserLockedOut; - _deviceManager.CameraImageUploaded += OnCameraImageUploaded; - return Task.CompletedTask; } - private void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e) + private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("CameraImageUploadedFrom"), - e.Argument.Device.Name), - Type = NotificationType.CameraImageUploaded.ToString() - }); - } - - private void OnUserLockedOut(object sender, GenericEventArgs<User> e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserLockedOutWithName"), - e.Argument.Name), - Type = NotificationType.UserLockedOut.ToString(), - UserId = e.Argument.Id - }); + await CreateLogEntry(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserLockedOutWithName"), + e.Argument.Name), + NotificationType.UserLockedOut.ToString(), + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) + private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), e.Provider, - Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)), - Type = "SubtitleDownloadFailure", + Notifications.NotificationEntryPoint.GetItemName(e.Item)), + "SubtitleDownloadFailure", + Guid.Empty) + { ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), ShortOverview = e.Exception.Message - }); + }).ConfigureAwait(false); } - private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) + private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) { var item = e.MediaInfo; @@ -166,15 +148,19 @@ namespace Emby.Server.Implementations.Activity var user = e.Users[0]; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), - Type = GetPlaybackStoppedNotificationType(item.MediaType), - UserId = user.Id - }); + await CreateLogEntry(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), + user.Name, + GetItemName(item), + e.DeviceName), + GetPlaybackStoppedNotificationType(item.MediaType), + user.Id)) + .ConfigureAwait(false); } - private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) + private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) { var item = e.MediaInfo; @@ -197,17 +183,16 @@ namespace Emby.Server.Implementations.Activity var user = e.Users.First(); - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), - Type = GetPlaybackNotificationType(item.MediaType), - UserId = user.Id - }); + GetPlaybackNotificationType(item.MediaType), + user.Id)) + .ConfigureAwait(false); } private static string GetItemName(BaseItemDto item) @@ -257,236 +242,215 @@ namespace Emby.Server.Implementations.Activity return null; } - private void OnSessionEnded(object sender, SessionEventArgs e) + private async void OnSessionEnded(object sender, SessionEventArgs e) { - string name; var session = e.SessionInfo; if (string.IsNullOrEmpty(session.UserName)) { - name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("DeviceOfflineWithName"), - session.DeviceName); - - // Causing too much spam for now return; } - else - { - name = string.Format( + + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserOfflineFromDevice"), session.UserName, - session.DeviceName); - } - - CreateLogEntry(new ActivityLogEntry + session.DeviceName), + "SessionEnded", + session.UserId) { - Name = name, - Type = "SessionEnded", ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), - UserId = session.UserId - }); + }).ConfigureAwait(false); } - private void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e) + private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e) { var user = e.Argument.User; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("AuthenticationSucceededWithUserName"), user.Name), - Type = "AuthenticationSucceeded", + "AuthenticationSucceeded", + user.Id) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.SessionInfo.RemoteEndPoint), - UserId = user.Id - }); + }).ConfigureAwait(false); } - private void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e) + private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("FailedLoginAttemptWithUserName"), e.Argument.Username), - Type = "AuthenticationFailed", + "AuthenticationFailed", + Guid.Empty) + { + LogSeverity = LogLevel.Error, ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.RemoteEndPoint), - Severity = LogLevel.Error - }); + }).ConfigureAwait(false); } - private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e) + private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPolicyUpdatedWithName"), e.Argument.Name), - Type = "UserPolicyUpdated", - UserId = e.Argument.Id - }); + "UserPolicyUpdated", + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnUserDeleted(object sender, GenericEventArgs<User> e) + private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Name), - Type = "UserDeleted" - }); + "UserDeleted", + Guid.Empty)) + .ConfigureAwait(false); } - private void OnUserPasswordChanged(object sender, GenericEventArgs<User> e) + private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPasswordChangedWithName"), e.Argument.Name), - Type = "UserPasswordChanged", - UserId = e.Argument.Id - }); + "UserPasswordChanged", + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnUserCreated(object sender, GenericEventArgs<User> e) + private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserCreatedWithName"), e.Argument.Name), - Type = "UserCreated", - UserId = e.Argument.Id - }); + "UserCreated", + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnSessionStarted(object sender, SessionEventArgs e) + private async void OnSessionStarted(object sender, SessionEventArgs e) { - string name; var session = e.SessionInfo; if (string.IsNullOrEmpty(session.UserName)) { - name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("DeviceOnlineWithName"), - session.DeviceName); - - // Causing too much spam for now return; } - else - { - name = string.Format( + + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserOnlineFromDevice"), session.UserName, - session.DeviceName); - } - - CreateLogEntry(new ActivityLogEntry + session.DeviceName), + "SessionStarted", + session.UserId) { - Name = name, - Type = "SessionStarted", ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), - session.RemoteEndPoint), - UserId = session.UserId - }); + session.RemoteEndPoint) + }).ConfigureAwait(false); } - private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, PackageVersionInfo)> e) + private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginUpdatedWithName"), e.Argument.Item1.Name), - Type = NotificationType.PluginUpdateInstalled.ToString(), + NotificationType.PluginUpdateInstalled.ToString(), + Guid.Empty) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), - e.Argument.Item2.versionStr), - Overview = e.Argument.Item2.description - }); + e.Argument.Item2.version), + Overview = e.Argument.Item2.changelog + }).ConfigureAwait(false); } - private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e) + private async void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), - Type = NotificationType.PluginUninstalled.ToString() - }); + NotificationType.PluginUninstalled.ToString(), + Guid.Empty)) + .ConfigureAwait(false); } - private void OnPluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e) + private async void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginInstalledWithName"), e.Argument.name), - Type = NotificationType.PluginInstalled.ToString(), + NotificationType.PluginInstalled.ToString(), + Guid.Empty) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), - e.Argument.versionStr) - }); + e.Argument.version) + }).ConfigureAwait(false); } - private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) + private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) { var installationInfo = e.InstallationInfo; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("NameInstallFailed"), installationInfo.Name), - Type = NotificationType.InstallationFailed.ToString(), + NotificationType.InstallationFailed.ToString(), + Guid.Empty) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), installationInfo.Version), Overview = e.Exception.Message - }); + }).ConfigureAwait(false); } - private void OnTaskCompleted(object sender, TaskCompletionEventArgs e) + private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e) { var result = e.Result; var task = e.Task; - var activityTask = task.ScheduledTask as IConfigurableScheduledTask; - if (activityTask != null && !activityTask.IsLogged) + if (task.ScheduledTask is IConfigurableScheduledTask activityTask + && !activityTask.IsLogged) { return; } @@ -511,22 +475,20 @@ namespace Emby.Server.Implementations.Activity vals.Add(e.Result.LongErrorMessage); } - CreateLogEntry(new ActivityLogEntry + await CreateLogEntry(new ActivityLog( + string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), + NotificationType.TaskFailed.ToString(), + Guid.Empty) { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("ScheduledTaskFailedWithName"), - task.Name), - Type = NotificationType.TaskFailed.ToString(), + LogSeverity = LogLevel.Error, Overview = string.Join(Environment.NewLine, vals), - ShortOverview = runningTime, - Severity = LogLevel.Error - }); + ShortOverview = runningTime + }).ConfigureAwait(false); } } - private void CreateLogEntry(ActivityLogEntry entry) - => _activityManager.Create(entry); + private async Task CreateLogEntry(ActivityLog entry) + => await _activityManager.CreateAsync(entry).ConfigureAwait(false); /// <inheritdoc /> public void Dispose() @@ -553,14 +515,12 @@ namespace Emby.Server.Implementations.Activity _userManager.UserDeleted -= OnUserDeleted; _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; _userManager.UserLockedOut -= OnUserLockedOut; - - _deviceManager.CameraImageUploaded -= OnCameraImageUploaded; } /// <summary> /// Constructs a user-friendly string for this TimeSpan instance. /// </summary> - public static string ToUserFriendlyString(TimeSpan span) + private static string ToUserFriendlyString(TimeSpan span) { const int DaysInYear = 365; const int DaysInMonth = 30; @@ -574,7 +534,7 @@ namespace Emby.Server.Implementations.Activity { int years = days / DaysInYear; values.Add(CreateValueString(years, "year")); - days = days % DaysInYear; + days %= DaysInYear; } // Number of months diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs deleted file mode 100644 index ee10845cfa..0000000000 --- a/Emby.Server.Implementations/Activity/ActivityManager.cs +++ /dev/null @@ -1,67 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Activity -{ - public class ActivityManager : IActivityManager - { - public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; - - private readonly IActivityRepository _repo; - private readonly ILogger _logger; - private readonly IUserManager _userManager; - - public ActivityManager( - ILoggerFactory loggerFactory, - IActivityRepository repo, - IUserManager userManager) - { - _logger = loggerFactory.CreateLogger(nameof(ActivityManager)); - _repo = repo; - _userManager = userManager; - } - - public void Create(ActivityLogEntry entry) - { - entry.Date = DateTime.UtcNow; - - _repo.Create(entry); - - EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(entry)); - } - - public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) - { - var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit); - - foreach (var item in result.Items) - { - if (item.UserId == Guid.Empty) - { - continue; - } - - var user = _userManager.GetUserById(item.UserId); - - if (user != null) - { - var dto = _userManager.GetUserDto(user); - item.UserPrimaryImageTag = dto.PrimaryImageTag; - } - } - - return result; - } - - public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) - { - return GetActivityLogEntries(minDate, null, startIndex, limit); - } - } -} diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs deleted file mode 100644 index 7be72319ea..0000000000 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ /dev/null @@ -1,313 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using Emby.Server.Implementations.Data; -using MediaBrowser.Controller; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Activity -{ - public class ActivityRepository : BaseSqliteRepository, IActivityRepository - { - private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - private readonly IFileSystem _fileSystem; - - public ActivityRepository(ILoggerFactory loggerFactory, IServerApplicationPaths appPaths, IFileSystem fileSystem) - : base(loggerFactory.CreateLogger(nameof(ActivityRepository))) - { - DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); - _fileSystem = fileSystem; - } - - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading database file. Will reset and retry."); - - _fileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - private void InitializeInternal() - { - using (var connection = GetConnection()) - { - connection.RunQueries(new[] - { - "create table if not exists ActivityLog (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)", - "drop index if exists idx_ActivityLogEntries" - }); - - TryMigrate(connection); - } - } - - private void TryMigrate(ManagedConnection connection) - { - try - { - if (TableExists(connection, "ActivityLogEntries")) - { - connection.RunQueries(new[] - { - "INSERT INTO ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) SELECT Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity FROM ActivityLogEntries", - "drop table if exists ActivityLogEntries" - }); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error migrating activity log database"); - } - } - - private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog"; - - public void Create(ActivityLogEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - using (var connection = GetConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)")) - { - statement.TryBind("@Name", entry.Name); - - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); - - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); - - statement.MoveNext(); - } - }, TransactionMode); - } - } - - public void Update(ActivityLogEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - using (var connection = GetConnection()) - { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id")) - { - statement.TryBind("@Id", entry.Id); - - statement.TryBind("@Name", entry.Name); - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); - - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); - - statement.MoveNext(); - } - }, TransactionMode); - } - } - - public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) - { - var commandText = BaseActivitySelectText; - var whereClauses = new List<string>(); - - if (minDate.HasValue) - { - whereClauses.Add("DateCreated>=@DateCreated"); - } - if (hasUserId.HasValue) - { - if (hasUserId.Value) - { - whereClauses.Add("UserId not null"); - } - else - { - whereClauses.Add("UserId is null"); - } - } - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - if (startIndex.HasValue && startIndex.Value > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - whereClauses.Add( - string.Format( - CultureInfo.InvariantCulture, - "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})", - pagingWhereText, - startIndex.Value)); - } - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - commandText += whereText; - - commandText += " ORDER BY DateCreated DESC"; - - if (limit.HasValue) - { - commandText += " LIMIT " + limit.Value.ToString(_usCulture); - } - - var statementTexts = new[] - { - commandText, - "select count (Id) from ActivityLog" + whereTextWithoutPaging - }; - - var list = new List<ActivityLogEntry>(); - var result = new QueryResult<ActivityLogEntry>(); - - using (var connection = GetConnection(true)) - { - connection.RunInTransaction( - db => - { - var statements = PrepareAll(db, statementTexts).ToList(); - - using (var statement = statements[0]) - { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetEntry(row)); - } - } - - using (var statement = statements[1]) - { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } - - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); - } - }, - ReadTransactionMode); - } - - result.Items = list; - return result; - } - - private static ActivityLogEntry GetEntry(IReadOnlyList<IResultSetValue> reader) - { - var index = 0; - - var info = new ActivityLogEntry - { - Id = reader[index].ToInt64() - }; - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Name = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Overview = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.ShortOverview = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Type = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.ItemId = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.UserId = new Guid(reader[index].ToString()); - } - - index++; - info.Date = reader[index].ReadDateTime(); - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Severity = (LogLevel)Enum.Parse(typeof(LogLevel), reader[index].ToString(), true); - } - - return info; - } - } -} diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index bc47817438..2adc1d6c34 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -15,6 +15,11 @@ namespace Emby.Server.Implementations.AppBase /// <summary> /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class. /// </summary> + /// <param name="programDataPath">The program data path.</param> + /// <param name="logDirectoryPath">The log directory path.</param> + /// <param name="configurationDirectoryPath">The configuration directory path.</param> + /// <param name="cacheDirectoryPath">The cache directory path.</param> + /// <param name="webDirectoryPath">The web directory path.</param> protected BaseApplicationPaths( string programDataPath, string logDirectoryPath, diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs index 854d7b4cbf..0b681fddfc 100644 --- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs +++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs @@ -36,24 +36,22 @@ namespace Emby.Server.Implementations.AppBase configuration = Activator.CreateInstance(type); } - using (var stream = new MemoryStream()) - { - xmlSerializer.SerializeToStream(configuration, stream); - - // Take the object we just got and serialize it back to bytes - var newBytes = stream.ToArray(); + using var stream = new MemoryStream(); + xmlSerializer.SerializeToStream(configuration, stream); - // If the file didn't exist before, or if something has changed, re-save - if (buffer == null || !buffer.SequenceEqual(newBytes)) - { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + // Take the object we just got and serialize it back to bytes + var newBytes = stream.ToArray(); - // Save it after load in case we got new items - File.WriteAllBytes(path, newBytes); - } + // If the file didn't exist before, or if something has changed, re-save + if (buffer == null || !buffer.SequenceEqual(newBytes)) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); - return configuration; + // Save it after load in case we got new items + File.WriteAllBytes(path, newBytes); } + + return configuration; } } } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index de044a4aa2..0a349bb331 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -22,7 +22,6 @@ using Emby.Dlna.Ssdp; using Emby.Drawing; using Emby.Notifications; using Emby.Photos; -using Emby.Server.Implementations.Activity; using Emby.Server.Implementations.Archiving; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; @@ -44,9 +43,9 @@ using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Session; -using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; +using Emby.Server.Implementations.SyncPlay; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -81,13 +80,12 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.TV; +using MediaBrowser.Controller.SyncPlay; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; -using MediaBrowser.Model.Activity; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -96,7 +94,6 @@ using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; @@ -104,11 +101,11 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Prometheus.DotNetRuntime; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; +using Emby.Server.Implementations.QuickConnect; namespace Emby.Server.Implementations { @@ -122,14 +119,20 @@ namespace Emby.Server.Implementations /// </summary> private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; - private SqliteUserRepository _userRepository; - private SqliteDisplayPreferencesRepository _displayPreferencesRepository; + private readonly IFileSystem _fileSystemManager; + private readonly INetworkManager _networkManager; + private readonly IXmlSerializer _xmlSerializer; + private readonly IStartupOptions _startupOptions; + + private IMediaEncoder _mediaEncoder; + private ISessionManager _sessionManager; + private IHttpServer _httpServer; + private IHttpClient _httpClient; /// <summary> /// Gets a value indicating whether this instance can self restart. /// </summary> - /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value> - public abstract bool CanSelfRestart { get; } + public bool CanSelfRestart => _startupOptions.RestartPath != null; public virtual bool CanLaunchWebBrowser { @@ -140,7 +143,7 @@ namespace Emby.Server.Implementations return false; } - if (StartupOptions.IsService) + if (_startupOptions.IsService) { return false; } @@ -210,21 +213,6 @@ namespace Emby.Server.Implementations /// <value>The configuration manager.</value> protected IConfigurationManager ConfigurationManager { get; set; } - public IFileSystem FileSystemManager { get; set; } - - /// <inheritdoc /> - public PackageVersionClass SystemUpdateLevel - { - get - { -#if BETA - return PackageVersionClass.Beta; -#else - return PackageVersionClass.Release; -#endif - } - } - /// <summary> /// Gets or sets the service provider. /// </summary> @@ -246,110 +234,6 @@ namespace Emby.Server.Implementations /// <value>The server configuration manager.</value> public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager; - /// <summary> - /// Gets or sets the user manager. - /// </summary> - /// <value>The user manager.</value> - public IUserManager UserManager { get; set; } - - /// <summary> - /// Gets or sets the library manager. - /// </summary> - /// <value>The library manager.</value> - internal ILibraryManager LibraryManager { get; set; } - - /// <summary> - /// Gets or sets the directory watchers. - /// </summary> - /// <value>The directory watchers.</value> - private ILibraryMonitor LibraryMonitor { get; set; } - - /// <summary> - /// Gets or sets the provider manager. - /// </summary> - /// <value>The provider manager.</value> - private IProviderManager ProviderManager { get; set; } - - /// <summary> - /// Gets or sets the HTTP server. - /// </summary> - /// <value>The HTTP server.</value> - private IHttpServer HttpServer { get; set; } - - private IDtoService DtoService { get; set; } - - public IImageProcessor ImageProcessor { get; set; } - - /// <summary> - /// Gets or sets the media encoder. - /// </summary> - /// <value>The media encoder.</value> - private IMediaEncoder MediaEncoder { get; set; } - - private ISubtitleEncoder SubtitleEncoder { get; set; } - - private ISessionManager SessionManager { get; set; } - - private ILiveTvManager LiveTvManager { get; set; } - - public LocalizationManager LocalizationManager { get; set; } - - private IEncodingManager EncodingManager { get; set; } - - private IChannelManager ChannelManager { get; set; } - - /// <summary> - /// Gets or sets the user data repository. - /// </summary> - /// <value>The user data repository.</value> - private IUserDataManager UserDataManager { get; set; } - - internal SqliteItemRepository ItemRepository { get; set; } - - private INotificationManager NotificationManager { get; set; } - - private ISubtitleManager SubtitleManager { get; set; } - - private IChapterManager ChapterManager { get; set; } - - private IDeviceManager DeviceManager { get; set; } - - internal IUserViewManager UserViewManager { get; set; } - - private IAuthenticationRepository AuthenticationRepository { get; set; } - - private ITVSeriesManager TVSeriesManager { get; set; } - - private ICollectionManager CollectionManager { get; set; } - - private IMediaSourceManager MediaSourceManager { get; set; } - - /// <summary> - /// Gets the installation manager. - /// </summary> - /// <value>The installation manager.</value> - protected IInstallationManager InstallationManager { get; private set; } - - protected IAuthService AuthService { get; private set; } - - public IStartupOptions StartupOptions { get; } - - internal IImageEncoder ImageEncoder { get; private set; } - - protected readonly IXmlSerializer XmlSerializer; - - protected ISocketFactory SocketFactory { get; private set; } - - protected ITaskManager TaskManager { get; private set; } - - public IHttpClient HttpClient { get; private set; } - - protected INetworkManager NetworkManager { get; set; } - - public IJsonSerializer JsonSerializer { get; private set; } - - protected IIsoManager IsoManager { get; private set; } - /// <summary> /// Initializes a new instance of the <see cref="ApplicationHost" /> class. /// </summary> @@ -358,29 +242,39 @@ namespace Emby.Server.Implementations ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - IImageEncoder imageEncoder, INetworkManager networkManager) { - XmlSerializer = new MyXmlSerializer(); + _xmlSerializer = new MyXmlSerializer(); - NetworkManager = networkManager; + _networkManager = networkManager; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; ApplicationPaths = applicationPaths; LoggerFactory = loggerFactory; - FileSystemManager = fileSystem; + _fileSystemManager = fileSystem; - ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, XmlSerializer, FileSystemManager); + ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); - Logger = LoggerFactory.CreateLogger("App"); + Logger = LoggerFactory.CreateLogger<ApplicationHost>(); - StartupOptions = options; + _startupOptions = options; - ImageEncoder = imageEncoder; + // Initialize runtime stat collection + if (ServerConfigurationManager.Configuration.EnableMetrics) + { + DotNetRuntimeStatsBuilder.Default().StartCollecting(); + } fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); - NetworkManager.NetworkChanged += OnNetworkChanged; + _networkManager.NetworkChanged += OnNetworkChanged; + + CertificateInfo = new CertificateInfo + { + Path = ServerConfigurationManager.Configuration.CertificatePath, + Password = ServerConfigurationManager.Configuration.CertificatePassword + }; + Certificate = GetCertificate(CertificateInfo); } public string ExpandVirtualPath(string path) @@ -448,10 +342,7 @@ namespace Emby.Server.Implementations } } - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> + /// <inheritdoc/> public string Name => ApplicationProductName; /// <summary> @@ -541,7 +432,7 @@ namespace Emby.Server.Implementations ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; - MediaEncoder.SetFFmpegPath(); + _mediaEncoder.SetFFmpegPath(); Logger.LogInformation("ServerId: {0}", SystemId); @@ -553,7 +444,7 @@ namespace Emby.Server.Implementations Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); - HttpServer.GlobalResponse = null; + _httpServer.GlobalResponse = null; stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); @@ -577,7 +468,7 @@ namespace Emby.Server.Implementations } /// <inheritdoc/> - public async Task InitAsync(IServiceCollection serviceCollection, IConfiguration startupConfig) + public void Init(IServiceCollection serviceCollection) { HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; @@ -589,8 +480,6 @@ namespace Emby.Server.Implementations HttpsPort = ServerConfiguration.DefaultHttpsPort; } - JsonSerializer = new JsonSerializer(); - if (Plugins != null) { var pluginBuilder = new StringBuilder(); @@ -610,41 +499,19 @@ namespace Emby.Server.Implementations DiscoverTypes(); - await RegisterServices(serviceCollection, startupConfig).ConfigureAwait(false); + RegisterServices(serviceCollection); } - public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next) - { - if (!context.WebSockets.IsWebSocketRequest) - { - await next().ConfigureAwait(false); - return; - } - - await HttpServer.ProcessWebSocketRequest(context).ConfigureAwait(false); - } - - public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next) - { - if (context.WebSockets.IsWebSocketRequest) - { - await next().ConfigureAwait(false); - return; - } - - var request = context.Request; - var response = context.Response; - var localPath = context.Request.Path.ToString(); - - var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>()); - await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false); - } + public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next) + => _httpServer.RequestHandler(context); /// <summary> /// Registers services/resources with the service collection that will be available via DI. /// </summary> - protected async Task RegisterServices(IServiceCollection serviceCollection, IConfiguration startupConfig) + protected virtual void RegisterServices(IServiceCollection serviceCollection) { + serviceCollection.AddSingleton(_startupOptions); + serviceCollection.AddMemoryCache(); serviceCollection.AddSingleton(ConfigurationManager); @@ -652,233 +519,161 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); - serviceCollection.AddSingleton(JsonSerializer); - - // TODO: Support for injecting ILogger should be deprecated in favour of ILogger<T> and this removed - serviceCollection.AddSingleton<ILogger>(Logger); + serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>(); - serviceCollection.AddSingleton(FileSystemManager); + serviceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton<TvdbClientManager>(); - HttpClient = new HttpClientManager.HttpClientManager( - ApplicationPaths, - LoggerFactory.CreateLogger<HttpClientManager.HttpClientManager>(), - FileSystemManager, - () => ApplicationUserAgent); - serviceCollection.AddSingleton(HttpClient); + serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>(); - serviceCollection.AddSingleton(NetworkManager); + serviceCollection.AddSingleton(_networkManager); - IsoManager = new IsoManager(); - serviceCollection.AddSingleton(IsoManager); + serviceCollection.AddSingleton<IIsoManager, IsoManager>(); - TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, LoggerFactory, FileSystemManager); - serviceCollection.AddSingleton(TaskManager); + serviceCollection.AddSingleton<ITaskManager, TaskManager>(); - serviceCollection.AddSingleton(XmlSerializer); + serviceCollection.AddSingleton(_xmlSerializer); - serviceCollection.AddSingleton(typeof(IStreamHelper), typeof(StreamHelper)); + serviceCollection.AddSingleton<IStreamHelper, StreamHelper>(); - var cryptoProvider = new CryptographyProvider(); - serviceCollection.AddSingleton<ICryptoProvider>(cryptoProvider); + serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>(); - SocketFactory = new SocketFactory(); - serviceCollection.AddSingleton(SocketFactory); + serviceCollection.AddSingleton<ISocketFactory, SocketFactory>(); - serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager)); + serviceCollection.AddSingleton<IInstallationManager, InstallationManager>(); - serviceCollection.AddSingleton(typeof(IZipClient), typeof(ZipClient)); + serviceCollection.AddSingleton<IZipClient, ZipClient>(); - serviceCollection.AddSingleton(typeof(IHttpResultFactory), typeof(HttpResultFactory)); + serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>(); serviceCollection.AddSingleton<IServerApplicationHost>(this); serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton(ServerConfigurationManager); - LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory.CreateLogger<LocalizationManager>()); - await LocalizationManager.LoadAll().ConfigureAwait(false); - serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager); - - serviceCollection.AddSingleton<IBlurayExaminer>(new BdInfoExaminer(FileSystemManager)); - - UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager); - serviceCollection.AddSingleton(UserDataManager); + serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); - _displayPreferencesRepository = new SqliteDisplayPreferencesRepository( - LoggerFactory.CreateLogger<SqliteDisplayPreferencesRepository>(), - ApplicationPaths, - FileSystemManager); - serviceCollection.AddSingleton<IDisplayPreferencesRepository>(_displayPreferencesRepository); + serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); - ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, LoggerFactory.CreateLogger<SqliteItemRepository>(), LocalizationManager); - serviceCollection.AddSingleton<IItemRepository>(ItemRepository); + serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); + serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); - AuthenticationRepository = GetAuthenticationRepository(); - serviceCollection.AddSingleton(AuthenticationRepository); + serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>(); - _userRepository = GetUserRepository(); + serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>(); - UserManager = new UserManager( - LoggerFactory.CreateLogger<UserManager>(), - _userRepository, - XmlSerializer, - NetworkManager, - () => ImageProcessor, - () => DtoService, - this, - JsonSerializer, - FileSystemManager, - cryptoProvider); + serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); - serviceCollection.AddSingleton(UserManager); + serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>(); - MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( - LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(), - ServerConfigurationManager, - FileSystemManager, - LocalizationManager, - () => SubtitleEncoder, - startupConfig, - StartupOptions.FFmpegPath); - serviceCollection.AddSingleton(MediaEncoder); + // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required + serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>)); + serviceCollection.AddSingleton<IUserManager, UserManager>(); - LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager, MediaEncoder); - serviceCollection.AddSingleton(LibraryManager); + // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required + // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation + serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>)); + serviceCollection.AddSingleton<IMediaEncoder>(provider => + ActivatorUtilities.CreateInstance<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(provider, _startupOptions.FFmpegPath ?? string.Empty)); - var musicManager = new MusicManager(LibraryManager); - serviceCollection.AddSingleton<IMusicManager>(musicManager); + // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required + serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); + serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>)); + serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>)); + serviceCollection.AddSingleton<ILibraryManager, LibraryManager>(); - LibraryMonitor = new LibraryMonitor(LoggerFactory, LibraryManager, ServerConfigurationManager, FileSystemManager); - serviceCollection.AddSingleton(LibraryMonitor); + serviceCollection.AddSingleton<IMusicManager, MusicManager>(); - serviceCollection.AddSingleton<ISearchEngine>(new SearchEngine(LoggerFactory, LibraryManager, UserManager)); + serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>(); - CertificateInfo = GetCertificateInfo(true); - Certificate = GetCertificate(CertificateInfo); + serviceCollection.AddSingleton<ISearchEngine, SearchEngine>(); serviceCollection.AddSingleton<ServiceController>(); - serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>(); serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>(); - ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); - serviceCollection.AddSingleton(ImageProcessor); + serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); - TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager); - serviceCollection.AddSingleton(TVSeriesManager); + serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>(); - DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager); - serviceCollection.AddSingleton(DeviceManager); + serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); - MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder); - serviceCollection.AddSingleton(MediaSourceManager); + serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>(); - SubtitleManager = new SubtitleManager(LoggerFactory, FileSystemManager, LibraryMonitor, MediaSourceManager, LocalizationManager); - serviceCollection.AddSingleton(SubtitleManager); + serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>(); - ProviderManager = new ProviderManager(HttpClient, SubtitleManager, ServerConfigurationManager, LibraryMonitor, LoggerFactory, FileSystemManager, ApplicationPaths, () => LibraryManager, JsonSerializer); - serviceCollection.AddSingleton(ProviderManager); + serviceCollection.AddSingleton<IProviderManager, ProviderManager>(); - DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager); - serviceCollection.AddSingleton(DtoService); + // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required + serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>)); + serviceCollection.AddSingleton<IDtoService, DtoService>(); - ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager); - serviceCollection.AddSingleton(ChannelManager); + serviceCollection.AddSingleton<IChannelManager, ChannelManager>(); - SessionManager = new SessionManager( - LoggerFactory.CreateLogger<SessionManager>(), - UserDataManager, - LibraryManager, - UserManager, - musicManager, - DtoService, - ImageProcessor, - this, - AuthenticationRepository, - DeviceManager, - MediaSourceManager); - serviceCollection.AddSingleton(SessionManager); + serviceCollection.AddSingleton<ISessionManager, SessionManager>(); - serviceCollection.AddSingleton<IDlnaManager>( - new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this)); + serviceCollection.AddSingleton<IDlnaManager, DlnaManager>(); - CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager); - serviceCollection.AddSingleton(CollectionManager); + serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); - serviceCollection.AddSingleton(typeof(IPlaylistManager), typeof(PlaylistManager)); + serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); - LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, LoggerFactory, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, FileSystemManager, () => ChannelManager); - serviceCollection.AddSingleton(LiveTvManager); + serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); - UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager); - serviceCollection.AddSingleton(UserViewManager); + serviceCollection.AddSingleton<LiveTvDtoService>(); + serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); - NotificationManager = new NotificationManager( - LoggerFactory.CreateLogger<NotificationManager>(), - UserManager, - ServerConfigurationManager); - serviceCollection.AddSingleton(NotificationManager); + serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); - serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager)); + serviceCollection.AddSingleton<INotificationManager, NotificationManager>(); - ChapterManager = new ChapterManager(ItemRepository); - serviceCollection.AddSingleton(ChapterManager); + serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); - EncodingManager = new MediaEncoder.EncodingManager( - LoggerFactory.CreateLogger<MediaEncoder.EncodingManager>(), - FileSystemManager, - MediaEncoder, - ChapterManager, - LibraryManager); - serviceCollection.AddSingleton(EncodingManager); + serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); - var activityLogRepo = GetActivityLogRepository(); - serviceCollection.AddSingleton(activityLogRepo); - serviceCollection.AddSingleton<IActivityManager>(new ActivityManager(LoggerFactory, activityLogRepo, UserManager)); + serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); - var authContext = new AuthorizationContext(AuthenticationRepository, UserManager); - serviceCollection.AddSingleton<IAuthorizationContext>(authContext); - serviceCollection.AddSingleton<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager)); + serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>(); + serviceCollection.AddSingleton<ISessionContext, SessionContext>(); - AuthService = new AuthService(LoggerFactory.CreateLogger<AuthService>(), authContext, ServerConfigurationManager, SessionManager, NetworkManager); - serviceCollection.AddSingleton(AuthService); + serviceCollection.AddSingleton<IAuthService, AuthService>(); + serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); - SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder( - LibraryManager, - LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(), - ApplicationPaths, - FileSystemManager, - MediaEncoder, - HttpClient, - MediaSourceManager); - serviceCollection.AddSingleton(SubtitleEncoder); + serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); - serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager)); + serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>(); serviceCollection.AddSingleton<EncodingHelper>(); - serviceCollection.AddSingleton(typeof(IAttachmentExtractor), typeof(MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor)); + serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); + } - serviceCollection.AddSingleton(typeof(IQuickConnect), typeof(QuickConnect.QuickConnectManager)); - _displayPreferencesRepository.Initialize(); + /// <summary> + /// Create services registered with the service container that need to be initialized at application startup. + /// </summary> + /// <returns>A task representing the service initialization operation.</returns> + public async Task InitializeServices() + { + var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); + await localizationManager.LoadAll().ConfigureAwait(false); + + _mediaEncoder = Resolve<IMediaEncoder>(); + _sessionManager = Resolve<ISessionManager>(); + _httpServer = Resolve<IHttpServer>(); + _httpClient = Resolve<IHttpClient>(); - var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger<SqliteUserDataRepository>(), ApplicationPaths); + ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize(); + ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); + ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize(); SetStaticProperties(); - ((UserManager)UserManager).Initialize(); + var userManager = (UserManager)Resolve<IUserManager>(); + userManager.Initialize(); - ((UserDataManager)UserDataManager).Repository = userDataRepo; - ItemRepository.Initialize(userDataRepo, UserManager); - ((LibraryManager)LibraryManager).ItemRepository = ItemRepository; - } + var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>(); + ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager); - /// <summary> - /// Create services registered with the service container that need to be initialized at application startup. - /// </summary> - public void InitializeServices() - { - HttpServer = Resolve<IHttpServer>(); + FindParts(); } public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) @@ -947,75 +742,38 @@ namespace Emby.Server.Implementations } } - /// <summary> - /// Gets the user repository. - /// </summary> - /// <returns><see cref="Task{SqliteUserRepository}" />.</returns> - private SqliteUserRepository GetUserRepository() - { - var repo = new SqliteUserRepository( - LoggerFactory.CreateLogger<SqliteUserRepository>(), - ApplicationPaths); - - repo.Initialize(); - - return repo; - } - - private IAuthenticationRepository GetAuthenticationRepository() - { - var repo = new AuthenticationRepository(LoggerFactory, ServerConfigurationManager); - - repo.Initialize(); - - return repo; - } - - private IActivityRepository GetActivityLogRepository() - { - var repo = new ActivityRepository(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager); - - repo.Initialize(); - - return repo; - } - /// <summary> /// Dirty hacks. /// </summary> private void SetStaticProperties() { - ItemRepository.ImageProcessor = ImageProcessor; - // For now there's no real way to inject these properly - BaseItem.Logger = LoggerFactory.CreateLogger("BaseItem"); + BaseItem.Logger = Resolve<ILogger<BaseItem>>(); BaseItem.ConfigurationManager = ServerConfigurationManager; - BaseItem.LibraryManager = LibraryManager; - BaseItem.ProviderManager = ProviderManager; - BaseItem.LocalizationManager = LocalizationManager; - BaseItem.ItemRepository = ItemRepository; - User.UserManager = UserManager; - BaseItem.FileSystem = FileSystemManager; - BaseItem.UserDataManager = UserDataManager; - BaseItem.ChannelManager = ChannelManager; - Video.LiveTvManager = LiveTvManager; - Folder.UserViewManager = UserViewManager; - UserView.TVSeriesManager = TVSeriesManager; - UserView.CollectionManager = CollectionManager; - BaseItem.MediaSourceManager = MediaSourceManager; - CollectionFolder.XmlSerializer = XmlSerializer; - CollectionFolder.JsonSerializer = JsonSerializer; + BaseItem.LibraryManager = Resolve<ILibraryManager>(); + BaseItem.ProviderManager = Resolve<IProviderManager>(); + BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); + BaseItem.ItemRepository = Resolve<IItemRepository>(); + User.UserManager = Resolve<IUserManager>(); + BaseItem.FileSystem = _fileSystemManager; + BaseItem.UserDataManager = Resolve<IUserDataManager>(); + BaseItem.ChannelManager = Resolve<IChannelManager>(); + Video.LiveTvManager = Resolve<ILiveTvManager>(); + Folder.UserViewManager = Resolve<IUserViewManager>(); + UserView.TVSeriesManager = Resolve<ITVSeriesManager>(); + UserView.CollectionManager = Resolve<ICollectionManager>(); + BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>(); + CollectionFolder.XmlSerializer = _xmlSerializer; + CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>(); CollectionFolder.ApplicationHost = this; - AuthenticatedAttribute.AuthService = AuthService; + AuthenticatedAttribute.AuthService = Resolve<IAuthService>(); } /// <summary> - /// Finds the parts. + /// Finds plugin components and register them with the appropriate services. /// </summary> - public void FindParts() + private void FindParts() { - InstallationManager = ServiceProvider.GetService<IInstallationManager>(); - if (!ServerConfigurationManager.Configuration.IsPortAuthorized) { ServerConfigurationManager.Configuration.IsPortAuthorized = true; @@ -1028,34 +786,34 @@ namespace Emby.Server.Implementations .Where(i => i != null) .ToArray(); - HttpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes()); + _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes()); - LibraryManager.AddParts( + Resolve<ILibraryManager>().AddParts( GetExports<IResolverIgnoreRule>(), GetExports<IItemResolver>(), GetExports<IIntroProvider>(), GetExports<IBaseItemComparer>(), GetExports<ILibraryPostScanTask>()); - ProviderManager.AddParts( + Resolve<IProviderManager>().AddParts( GetExports<IImageProvider>(), GetExports<IMetadataService>(), GetExports<IMetadataProvider>(), GetExports<IMetadataSaver>(), GetExports<IExternalId>()); - LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); + Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); - SubtitleManager.AddParts(GetExports<ISubtitleProvider>()); + Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>()); - ChannelManager.AddParts(GetExports<IChannel>()); + Resolve<IChannelManager>().AddParts(GetExports<IChannel>()); - MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>()); + Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); - NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); - UserManager.AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>()); + Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); + Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>()); - IsoManager.AddParts(GetExports<IIsoMounter>()); + Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>()); } private IPlugin LoadPlugin(IPlugin plugin) @@ -1162,16 +920,6 @@ namespace Emby.Server.Implementations }); } - private CertificateInfo GetCertificateInfo(bool generateCertificate) - { - // Custom cert - return new CertificateInfo - { - Path = ServerConfigurationManager.Configuration.CertificatePath, - Password = ServerConfigurationManager.Configuration.CertificatePassword - }; - } - /// <summary> /// Called when [configuration updated]. /// </summary> @@ -1198,14 +946,13 @@ namespace Emby.Server.Implementations } } - if (!HttpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) + if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) { requiresRestart = true; } var currentCertPath = CertificateInfo?.Path; - var newCertInfo = GetCertificateInfo(false); - var newCertPath = newCertInfo?.Path; + var newCertPath = ServerConfigurationManager.Configuration.CertificatePath; if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase)) { @@ -1258,7 +1005,7 @@ namespace Emby.Server.Implementations { try { - await SessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false); + await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -1362,7 +1109,7 @@ namespace Emby.Server.Implementations IsShuttingDown = IsShuttingDown, Version = ApplicationVersionString, WebSocketPortNumber = HttpPort, - CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(), + CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(), Id = SystemId, ProgramDataPath = ApplicationPaths.ProgramDataPath, WebPath = ApplicationPaths.WebPath, @@ -1370,9 +1117,6 @@ namespace Emby.Server.Implementations ItemsByNamePath = ApplicationPaths.InternalMetadataPath, InternalMetadataPath = ApplicationPaths.InternalMetadataPath, CachePath = ApplicationPaths.CachePath, - HttpServerPortNumber = HttpPort, - SupportsHttps = SupportsHttps, - HttpsPortNumber = HttpsPort, OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystemDisplayName = OperatingSystem.Name, CanSelfRestart = CanSelfRestart, @@ -1382,15 +1126,14 @@ namespace Emby.Server.Implementations ServerName = FriendlyName, LocalAddress = localAddress, SupportsLibraryMonitor = true, - EncoderLocation = MediaEncoder.EncoderLocation, + EncoderLocation = _mediaEncoder.EncoderLocation, SystemArchitecture = RuntimeInformation.OSArchitecture, - SystemUpdateLevel = SystemUpdateLevel, - PackageName = StartupOptions.PackageName + PackageName = _startupOptions.PackageName }; } public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo() - => NetworkManager.GetMacAddresses() + => _networkManager.GetMacAddresses() .Select(i => new WakeOnLanInfo(i)) .ToList(); @@ -1409,23 +1152,22 @@ namespace Emby.Server.Implementations }; } - public bool EnableHttps => SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps; - - public bool SupportsHttps => Certificate != null || ServerConfigurationManager.Configuration.IsBehindProxy; + /// <inheritdoc/> + public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; + /// <inheritdoc/> public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken) { try { // Return the first matched address, if found, or the first known local address var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false); - - foreach (var address in addresses) + if (addresses.Count == 0) { - return GetLocalApiUrl(address); + return null; } - return null; + return GetLocalApiUrl(addresses.First()); } catch (Exception ex) { @@ -1468,22 +1210,24 @@ namespace Emby.Server.Implementations return GetLocalApiUrl(ipAddress.ToString()); } - /// <inheritdoc /> - public string GetLocalApiUrl(ReadOnlySpan<char> host) + /// <inheritdoc/> + public string GetLoopbackHttpApiUrl() { - var url = new StringBuilder(64); - url.Append(EnableHttps ? "https://" : "http://") - .Append(host) - .Append(':') - .Append(EnableHttps ? HttpsPort : HttpPort); - - string baseUrl = ServerConfigurationManager.Configuration.BaseUrl; - if (baseUrl.Length != 0) - { - url.Append(baseUrl); - } + return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); + } - return url.ToString(); + /// <inheritdoc/> + public string GetLocalApiUrl(ReadOnlySpan<char> host, string scheme = null, int? port = null) + { + // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does + // not. For consistency, always trim the trailing slash. + return new UriBuilder + { + Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), + Host = host.ToString(), + Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), + Path = ServerConfigurationManager.Configuration.BaseUrl + }.ToString().TrimEnd('/'); } public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken) @@ -1502,7 +1246,7 @@ namespace Emby.Server.Implementations if (addresses.Count == 0) { - addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); + addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); } var resultList = new List<IPAddress>(); @@ -1517,7 +1261,7 @@ namespace Emby.Server.Implementations } } - var valid = await IsIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); + var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); if (valid) { resultList.Add(address); @@ -1551,7 +1295,7 @@ namespace Emby.Server.Implementations private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase); - private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) + private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) { if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback)) @@ -1559,8 +1303,7 @@ namespace Emby.Server.Implementations return true; } - var apiUrl = GetLocalApiUrl(address); - apiUrl += "/system/ping"; + var apiUrl = GetLocalApiUrl(address) + "/system/ping"; if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult)) { @@ -1569,7 +1312,7 @@ namespace Emby.Server.Implementations try { - using (var response = await HttpClient.SendAsync( + using (var response = await _httpClient.SendAsync( new HttpRequestOptions { Url = apiUrl, @@ -1622,7 +1365,7 @@ namespace Emby.Server.Implementations try { - await SessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false); + await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -1743,14 +1486,8 @@ namespace Emby.Server.Implementations Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name); } } - - _userRepository?.Dispose(); - _displayPreferencesRepository?.Dispose(); } - _userRepository = null; - _displayPreferencesRepository = null; - _disposed = true; } } diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs index 4a6e5cfd75..591ae547d6 100644 --- a/Emby.Server.Implementations/Archiving/ZipClient.cs +++ b/Emby.Server.Implementations/Archiving/ZipClient.cs @@ -22,10 +22,8 @@ namespace Emby.Server.Implementations.Archiving /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles) { - using (var fileStream = File.OpenRead(sourceFile)) - { - ExtractAll(fileStream, targetPath, overwriteExistingFiles); - } + using var fileStream = File.OpenRead(sourceFile); + ExtractAll(fileStream, targetPath, overwriteExistingFiles); } /// <summary> @@ -36,67 +34,61 @@ namespace Emby.Server.Implementations.Archiving /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles) { - using (var reader = ReaderFactory.Open(source)) + using var reader = ReaderFactory.Open(source); + var options = new ExtractionOptions { - var options = new ExtractionOptions(); - options.ExtractFullPath = true; - - if (overwriteExistingFiles) - { - options.Overwrite = true; - } + ExtractFullPath = true + }; - reader.WriteAllToDirectory(targetPath, options); + if (overwriteExistingFiles) + { + options.Overwrite = true; } + + reader.WriteAllToDirectory(targetPath, options); } + /// <inheritdoc /> public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles) { - using (var reader = ZipReader.Open(source)) + using var reader = ZipReader.Open(source); + var options = new ExtractionOptions { - var options = new ExtractionOptions(); - options.ExtractFullPath = true; + ExtractFullPath = true, + Overwrite = overwriteExistingFiles + }; - if (overwriteExistingFiles) - { - options.Overwrite = true; - } - - reader.WriteAllToDirectory(targetPath, options); - } + reader.WriteAllToDirectory(targetPath, options); } + /// <inheritdoc /> public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles) { - using (var reader = GZipReader.Open(source)) + using var reader = GZipReader.Open(source); + var options = new ExtractionOptions { - var options = new ExtractionOptions(); - options.ExtractFullPath = true; + ExtractFullPath = true, + Overwrite = overwriteExistingFiles + }; - if (overwriteExistingFiles) - { - options.Overwrite = true; - } - - reader.WriteAllToDirectory(targetPath, options); - } + reader.WriteAllToDirectory(targetPath, options); } + /// <inheritdoc /> public void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName) { - using (var reader = GZipReader.Open(source)) + using var reader = GZipReader.Open(source); + if (reader.MoveToNextEntry()) { - if (reader.MoveToNextEntry()) + var entry = reader.Entry; + + var filename = entry.Key; + if (string.IsNullOrWhiteSpace(filename)) { - var entry = reader.Entry; - - var filename = entry.Key; - if (string.IsNullOrWhiteSpace(filename)) - { - filename = defaultFileName; - } - reader.WriteEntryToFile(Path.Combine(targetPath, filename)); + filename = defaultFileName; } + + reader.WriteEntryToFile(Path.Combine(targetPath, filename)); } } @@ -108,10 +100,8 @@ namespace Emby.Server.Implementations.Archiving /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles) { - using (var fileStream = File.OpenRead(sourceFile)) - { - ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles); - } + using var fileStream = File.OpenRead(sourceFile); + ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles); } /// <summary> @@ -122,21 +112,15 @@ namespace Emby.Server.Implementations.Archiving /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles) { - using (var archive = SevenZipArchive.Open(source)) + using var archive = SevenZipArchive.Open(source); + using var reader = archive.ExtractAllEntries(); + var options = new ExtractionOptions { - using (var reader = archive.ExtractAllEntries()) - { - var options = new ExtractionOptions(); - options.ExtractFullPath = true; - - if (overwriteExistingFiles) - { - options.Overwrite = true; - } + ExtractFullPath = true, + Overwrite = overwriteExistingFiles + }; - reader.WriteAllToDirectory(targetPath, options); - } - } + reader.WriteAllToDirectory(targetPath, options); } /// <summary> @@ -147,10 +131,8 @@ namespace Emby.Server.Implementations.Archiving /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles) { - using (var fileStream = File.OpenRead(sourceFile)) - { - ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles); - } + using var fileStream = File.OpenRead(sourceFile); + ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles); } /// <summary> @@ -161,21 +143,15 @@ namespace Emby.Server.Implementations.Archiving /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param> public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles) { - using (var archive = TarArchive.Open(source)) + using var archive = TarArchive.Open(source); + using var reader = archive.ExtractAllEntries(); + var options = new ExtractionOptions { - using (var reader = archive.ExtractAllEntries()) - { - var options = new ExtractionOptions(); - options.ExtractFullPath = true; + ExtractFullPath = true, + Overwrite = overwriteExistingFiles + }; - if (overwriteExistingFiles) - { - options.Overwrite = true; - } - - reader.WriteAllToDirectory(targetPath, options); - } - } + reader.WriteAllToDirectory(targetPath, options); } } } diff --git a/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs b/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs index 93000ae127..7ae26bd8b4 100644 --- a/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs +++ b/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs @@ -1,13 +1,15 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Branding; namespace Emby.Server.Implementations.Branding { + /// <summary> + /// A configuration factory for <see cref="BrandingOptions"/>. + /// </summary> public class BrandingConfigurationFactory : IConfigurationFactory { + /// <inheritdoc /> public IEnumerable<ConfigurationStore> GetConfigurations() { return new[] diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 96096e142a..7f7c6a0be4 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -31,18 +31,18 @@ namespace Emby.Server.Implementations.Browser /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored. /// </summary> /// <param name="appHost">The application host.</param> - /// <param name="url">The URL.</param> - private static void TryOpenUrl(IServerApplicationHost appHost, string url) + /// <param name="relativeUrl">The URL to open, relative to the server base URL.</param> + private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl) { try { string baseUrl = appHost.GetLocalApiUrl("localhost"); - appHost.LaunchUrl(baseUrl + url); + appHost.LaunchUrl(baseUrl + relativeUrl); } catch (Exception ex) { var logger = appHost.Resolve<ILogger>(); - logger?.LogError(ex, "Failed to open browser window with URL {URL}", url); + logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl); } } } diff --git a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs index 6016fed079..3e149cc82c 100644 --- a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs +++ b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs @@ -1,7 +1,6 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Channels; @@ -11,6 +10,9 @@ using MediaBrowser.Model.Dto; namespace Emby.Server.Implementations.Channels { + /// <summary> + /// A media source provider for channels. + /// </summary> public class ChannelDynamicMediaSourceProvider : IMediaSourceProvider { private readonly ChannelManager _channelManager; @@ -27,12 +29,9 @@ namespace Emby.Server.Implementations.Channels /// <inheritdoc /> public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken) { - if (item.SourceType == SourceType.Channel) - { - return _channelManager.GetDynamicMediaSources(item, cancellationToken); - } - - return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>()); + return item.SourceType == SourceType.Channel + ? _channelManager.GetDynamicMediaSources(item, cancellationToken) + : Task.FromResult(Enumerable.Empty<MediaSourceInfo>()); } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs index 62aeb9bcb9..25cbfcf146 100644 --- a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs +++ b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using System.Linq; using System.Threading; @@ -11,20 +9,32 @@ using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Channels { + /// <summary> + /// An image provider for channels. + /// </summary> public class ChannelImageProvider : IDynamicImageProvider, IHasItemChangeMonitor { private readonly IChannelManager _channelManager; + /// <summary> + /// Initializes a new instance of the <see cref="ChannelImageProvider"/> class. + /// </summary> + /// <param name="channelManager">The channel manager.</param> public ChannelImageProvider(IChannelManager channelManager) { _channelManager = channelManager; } + /// <inheritdoc /> + public string Name => "Channel Image Provider"; + + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return GetChannel(item).GetSupportedChannelImages(); } + /// <inheritdoc /> public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken) { var channel = GetChannel(item); @@ -32,8 +42,7 @@ namespace Emby.Server.Implementations.Channels return channel.GetChannelImage(type, cancellationToken); } - public string Name => "Channel Image Provider"; - + /// <inheritdoc /> public bool Supports(BaseItem item) { return item is Channel; @@ -46,6 +55,7 @@ namespace Emby.Server.Implementations.Channels return ((ChannelManager)_channelManager).GetChannelProvider(channel); } + /// <inheritdoc /> public bool HasChanged(BaseItem item, IDirectoryService directoryService) { return GetSupportedImages(item).Any(i => !item.HasImage(i)); diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 6e1baddfed..138832fb86 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -29,10 +27,11 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Channels { + /// <summary> + /// The LiveTV channel manager. + /// </summary> public class ChannelManager : IChannelManager { - internal IChannel[] Channels { get; private set; } - private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; private readonly IDtoService _dtoService; @@ -43,11 +42,28 @@ namespace Emby.Server.Implementations.Channels private readonly IJsonSerializer _jsonSerializer; private readonly IProviderManager _providerManager; + private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo = + new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>(); + + private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); + + /// <summary> + /// Initializes a new instance of the <see cref="ChannelManager"/> class. + /// </summary> + /// <param name="userManager">The user manager.</param> + /// <param name="dtoService">The dto service.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="loggerFactory">The logger factory.</param> + /// <param name="config">The server configuration manager.</param> + /// <param name="fileSystem">The filesystem.</param> + /// <param name="userDataManager">The user data manager.</param> + /// <param name="jsonSerializer">The JSON serializer.</param> + /// <param name="providerManager">The provider manager.</param> public ChannelManager( IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, - ILoggerFactory loggerFactory, + ILogger<ChannelManager> logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, @@ -57,7 +73,7 @@ namespace Emby.Server.Implementations.Channels _userManager = userManager; _dtoService = dtoService; _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(nameof(ChannelManager)); + _logger = logger; _config = config; _fileSystem = fileSystem; _userDataManager = userDataManager; @@ -65,13 +81,17 @@ namespace Emby.Server.Implementations.Channels _providerManager = providerManager; } + internal IChannel[] Channels { get; private set; } + private static TimeSpan CacheLength => TimeSpan.FromHours(3); + /// <inheritdoc /> public void AddParts(IEnumerable<IChannel> channels) { Channels = channels.ToArray(); } + /// <inheritdoc /> public bool EnableMediaSourceDisplay(BaseItem item) { var internalChannel = _libraryManager.GetItemById(item.ChannelId); @@ -80,15 +100,16 @@ namespace Emby.Server.Implementations.Channels return !(channel is IDisableMediaSourceDisplay); } + /// <inheritdoc /> public bool CanDelete(BaseItem item) { var internalChannel = _libraryManager.GetItemById(item.ChannelId); var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); - var supportsDelete = channel as ISupportsDelete; - return supportsDelete != null && supportsDelete.CanDelete(item); + return channel is ISupportsDelete supportsDelete && supportsDelete.CanDelete(item); } + /// <inheritdoc /> public bool EnableMediaProbe(BaseItem item) { var internalChannel = _libraryManager.GetItemById(item.ChannelId); @@ -97,6 +118,7 @@ namespace Emby.Server.Implementations.Channels return channel is ISupportsMediaProbe; } + /// <inheritdoc /> public Task DeleteItem(BaseItem item) { var internalChannel = _libraryManager.GetItemById(item.ChannelId); @@ -123,11 +145,16 @@ namespace Emby.Server.Implementations.Channels .OrderBy(i => i.Name); } + /// <summary> + /// Get the installed channel IDs. + /// </summary> + /// <returns>An <see cref="IEnumerable{T}"/> containing installed channel IDs.</returns> public IEnumerable<Guid> GetInstalledChannelIds() { return GetAllChannels().Select(i => GetInternalChannelId(i.Name)); } + /// <inheritdoc /> public QueryResult<Channel> GetChannelsInternal(ChannelQuery query) { var user = query.UserId.Equals(Guid.Empty) @@ -146,15 +173,13 @@ namespace Emby.Server.Implementations.Channels { try { - var hasAttributes = GetChannelProvider(i) as IHasFolderAttributes; - - return (hasAttributes != null && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val; + return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes + && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val; } catch { return false; } - }).ToList(); } @@ -171,7 +196,6 @@ namespace Emby.Server.Implementations.Channels { return false; } - }).ToList(); } @@ -188,9 +212,9 @@ namespace Emby.Server.Implementations.Channels { return false; } - }).ToList(); } + if (query.IsFavorite.HasValue) { var val = query.IsFavorite.Value; @@ -215,7 +239,6 @@ namespace Emby.Server.Implementations.Channels { return false; } - }).ToList(); } @@ -226,6 +249,7 @@ namespace Emby.Server.Implementations.Channels { all = all.Skip(query.StartIndex.Value).ToList(); } + if (query.Limit.HasValue) { all = all.Take(query.Limit.Value).ToList(); @@ -248,6 +272,7 @@ namespace Emby.Server.Implementations.Channels }; } + /// <inheritdoc /> public QueryResult<BaseItemDto> GetChannels(ChannelQuery query) { var user = query.UserId.Equals(Guid.Empty) @@ -256,11 +281,9 @@ namespace Emby.Server.Implementations.Channels var internalResult = GetChannelsInternal(query); - var dtoOptions = new DtoOptions() - { - }; + var dtoOptions = new DtoOptions(); - //TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues. + // TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues. var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user); var result = new QueryResult<BaseItemDto> @@ -272,6 +295,12 @@ namespace Emby.Server.Implementations.Channels return result; } + /// <summary> + /// Refreshes the associated channels. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> + /// <returns>The completed task.</returns> public async Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken) { var allChannelsList = GetAllChannels().ToList(); @@ -305,14 +334,7 @@ namespace Emby.Server.Implementations.Channels private Channel GetChannelEntity(IChannel channel) { - var item = GetChannel(GetInternalChannelId(channel.Name)); - - if (item == null) - { - item = GetChannel(channel, CancellationToken.None).Result; - } - - return item; + return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result; } private List<MediaSourceInfo> GetSavedMediaSources(BaseItem item) @@ -341,8 +363,8 @@ namespace Emby.Server.Implementations.Channels } catch { - } + return; } @@ -351,6 +373,7 @@ namespace Emby.Server.Implementations.Channels _jsonSerializer.SerializeToFile(mediaSources, path); } + /// <inheritdoc /> public IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken) { IEnumerable<MediaSourceInfo> results = GetSavedMediaSources(item); @@ -360,16 +383,20 @@ namespace Emby.Server.Implementations.Channels .ToList(); } + /// <summary> + /// Gets the dynamic media sources based on the provided item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> + /// <returns>The task representing the operation to get the media sources.</returns> public async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancellationToken) { var channel = GetChannel(item.ChannelId); var channelPlugin = GetChannelProvider(channel); - var requiresCallback = channelPlugin as IRequiresMediaInfoCallback; - IEnumerable<MediaSourceInfo> results; - if (requiresCallback != null) + if (channelPlugin is IRequiresMediaInfoCallback requiresCallback) { results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken) .ConfigureAwait(false); @@ -384,9 +411,6 @@ namespace Emby.Server.Implementations.Channels .ToList(); } - private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo = - new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>(); - private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken) { if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo)) @@ -409,7 +433,7 @@ namespace Emby.Server.Implementations.Channels private static MediaSourceInfo NormalizeMediaSource(BaseItem item, MediaSourceInfo info) { - info.RunTimeTicks = info.RunTimeTicks ?? item.RunTimeTicks; + info.RunTimeTicks ??= item.RunTimeTicks; return info; } @@ -444,18 +468,21 @@ namespace Emby.Server.Implementations.Channels { isNew = true; } + item.Path = path; if (!item.ChannelId.Equals(id)) { forceUpdate = true; } + item.ChannelId = id; if (item.ParentId != parentFolderId) { forceUpdate = true; } + item.ParentId = parentFolderId; item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating); @@ -472,51 +499,56 @@ namespace Emby.Server.Implementations.Channels _libraryManager.CreateItem(item, null); } - await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ForceSave = !isNew && forceUpdate - }, cancellationToken).ConfigureAwait(false); + await item.RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = !isNew && forceUpdate + }, + cancellationToken).ConfigureAwait(false); return item; } private static string GetOfficialRating(ChannelParentalRating rating) { - switch (rating) - { - case ChannelParentalRating.Adult: - return "XXX"; - case ChannelParentalRating.UsR: - return "R"; - case ChannelParentalRating.UsPG13: - return "PG-13"; - case ChannelParentalRating.UsPG: - return "PG"; - default: - return null; - } + return rating switch + { + ChannelParentalRating.Adult => "XXX", + ChannelParentalRating.UsR => "R", + ChannelParentalRating.UsPG13 => "PG-13", + ChannelParentalRating.UsPG => "PG", + _ => null + }; } + /// <summary> + /// Gets a channel with the provided Guid. + /// </summary> + /// <param name="id">The Guid.</param> + /// <returns>The corresponding channel.</returns> public Channel GetChannel(Guid id) { return _libraryManager.GetItemById(id) as Channel; } + /// <inheritdoc /> public Channel GetChannel(string id) { return _libraryManager.GetItemById(id) as Channel; } + /// <inheritdoc /> public ChannelFeatures[] GetAllChannelFeatures() { - return _libraryManager.GetItemIds(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Channel).Name }, - OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } - - }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); + return _libraryManager.GetItemIds( + new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Channel).Name }, + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } + }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); } + /// <inheritdoc /> public ChannelFeatures GetChannelFeatures(string id) { if (string.IsNullOrEmpty(id)) @@ -530,15 +562,27 @@ namespace Emby.Server.Implementations.Channels return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures()); } + /// <summary> + /// Checks whether the provided Guid supports external transfer. + /// </summary> + /// <param name="channelId">The Guid.</param> + /// <returns>Whether or not the provided Guid supports external transfer.</returns> public bool SupportsExternalTransfer(Guid channelId) { - //var channel = GetChannel(channelId); var channelProvider = GetChannelProvider(channelId); return channelProvider.GetChannelFeatures().SupportsContentDownloading; } - public ChannelFeatures GetChannelFeaturesDto(Channel channel, + /// <summary> + /// Gets the provided channel's supported features. + /// </summary> + /// <param name="channel">The channel.</param> + /// <param name="provider">The provider.</param> + /// <param name="features">The features.</param> + /// <returns>The supported features.</returns> + public ChannelFeatures GetChannelFeaturesDto( + Channel channel, IChannel provider, InternalChannelFeatures features) { @@ -567,9 +611,11 @@ namespace Emby.Server.Implementations.Channels { throw new ArgumentNullException(nameof(name)); } + return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel)); } + /// <inheritdoc /> public async Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken) { var internalResult = await GetLatestChannelItemsInternal(query, cancellationToken).ConfigureAwait(false); @@ -588,6 +634,7 @@ namespace Emby.Server.Implementations.Channels return result; } + /// <inheritdoc /> public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken) { var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray(); @@ -614,7 +661,7 @@ namespace Emby.Server.Implementations.Channels query.IsFolder = false; // hack for trailers, figure out a better way later - var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.IndexOf("Trailer") != -1; + var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringComparison.Ordinal); if (sortByPremiereDate) { @@ -640,10 +687,12 @@ namespace Emby.Server.Implementations.Channels { var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false); - var query = new InternalItemsQuery(); - query.Parent = internalChannel; - query.EnableTotalRecordCount = false; - query.ChannelIds = new Guid[] { internalChannel.Id }; + var query = new InternalItemsQuery + { + Parent = internalChannel, + EnableTotalRecordCount = false, + ChannelIds = new Guid[] { internalChannel.Id } + }; var result = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); @@ -651,17 +700,20 @@ namespace Emby.Server.Implementations.Channels { if (item is Folder folder) { - await GetChannelItemsInternal(new InternalItemsQuery - { - Parent = folder, - EnableTotalRecordCount = false, - ChannelIds = new Guid[] { internalChannel.Id } - - }, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); + await GetChannelItemsInternal( + new InternalItemsQuery + { + Parent = folder, + EnableTotalRecordCount = false, + ChannelIds = new Guid[] { internalChannel.Id } + }, + new SimpleProgress<double>(), + cancellationToken).ConfigureAwait(false); } } } + /// <inheritdoc /> public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken) { // Get the internal channel entity @@ -672,7 +724,8 @@ namespace Emby.Server.Implementations.Channels var parentItem = query.ParentId == Guid.Empty ? channel : _libraryManager.GetItemById(query.ParentId); - var itemsResult = await GetChannelItems(channelProvider, + var itemsResult = await GetChannelItems( + channelProvider, query.User, parentItem is Channel ? null : parentItem.ExternalId, null, @@ -684,13 +737,12 @@ namespace Emby.Server.Implementations.Channels { query.Parent = channel; } + query.ChannelIds = Array.Empty<Guid>(); // Not yet sure why this is causing a problem query.GroupByPresentationUniqueKey = false; - //_logger.LogDebug("GetChannelItemsInternal"); - // null if came from cache if (itemsResult != null) { @@ -707,12 +759,15 @@ namespace Emby.Server.Implementations.Channels var deadItem = _libraryManager.GetItemById(deadId); if (deadItem != null) { - _libraryManager.DeleteItem(deadItem, new DeleteOptions - { - DeleteFileLocation = false, - DeleteFromExternalProvider = false - - }, parentItem, false); + _libraryManager.DeleteItem( + deadItem, + new DeleteOptions + { + DeleteFileLocation = false, + DeleteFromExternalProvider = false + }, + parentItem, + false); } } } @@ -720,6 +775,7 @@ namespace Emby.Server.Implementations.Channels return _libraryManager.GetItemsResult(query); } + /// <inheritdoc /> public async Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken) { var internalResult = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); @@ -735,7 +791,6 @@ namespace Emby.Server.Implementations.Channels return result; } - private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); private async Task<ChannelItemResult> GetChannelItems(IChannel channel, User user, string externalFolderId, @@ -743,7 +798,7 @@ namespace Emby.Server.Implementations.Channels bool sortDescending, CancellationToken cancellationToken) { - var userId = user == null ? null : user.Id.ToString("N", CultureInfo.InvariantCulture); + var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture); var cacheLength = CacheLength; var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending); @@ -761,11 +816,9 @@ namespace Emby.Server.Implementations.Channels } catch (FileNotFoundException) { - } catch (IOException) { - } await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -785,16 +838,14 @@ namespace Emby.Server.Implementations.Channels } catch (FileNotFoundException) { - } catch (IOException) { - } var query = new InternalChannelItemQuery { - UserId = user == null ? Guid.Empty : user.Id, + UserId = user?.Id ?? Guid.Empty, SortBy = sortField, SortDescending = sortDescending, FolderId = externalFolderId @@ -833,7 +884,8 @@ namespace Emby.Server.Implementations.Channels } } - private string GetChannelDataCachePath(IChannel channel, + private string GetChannelDataCachePath( + IChannel channel, string userId, string externalFolderId, ChannelItemSortField? sortField, @@ -843,8 +895,7 @@ namespace Emby.Server.Implementations.Channels var userCacheKey = string.Empty; - var hasCacheKey = channel as IHasCacheKey; - if (hasCacheKey != null) + if (channel is IHasCacheKey hasCacheKey) { userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty; } @@ -858,6 +909,7 @@ namespace Emby.Server.Implementations.Channels { filename += "-sortField-" + sortField.Value; } + if (sortDescending) { filename += "-sortDescending"; @@ -865,7 +917,8 @@ namespace Emby.Server.Implementations.Channels filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture); - return Path.Combine(_config.ApplicationPaths.CachePath, + return Path.Combine( + _config.ApplicationPaths.CachePath, "channels", channelId, version, @@ -919,60 +972,32 @@ namespace Emby.Server.Implementations.Channels if (info.Type == ChannelItemType.Folder) { - if (info.FolderType == ChannelFolderType.MusicAlbum) - { - item = GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew); - } - else if (info.FolderType == ChannelFolderType.MusicArtist) - { - item = GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew); - } - else if (info.FolderType == ChannelFolderType.PhotoAlbum) - { - item = GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew); - } - else if (info.FolderType == ChannelFolderType.Series) - { - item = GetItemById<Series>(info.Id, channelProvider.Name, out isNew); - } - else if (info.FolderType == ChannelFolderType.Season) - { - item = GetItemById<Season>(info.Id, channelProvider.Name, out isNew); - } - else + item = info.FolderType switch { - item = GetItemById<Folder>(info.Id, channelProvider.Name, out isNew); - } + ChannelFolderType.MusicAlbum => GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew), + ChannelFolderType.MusicArtist => GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew), + ChannelFolderType.PhotoAlbum => GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew), + ChannelFolderType.Series => GetItemById<Series>(info.Id, channelProvider.Name, out isNew), + ChannelFolderType.Season => GetItemById<Season>(info.Id, channelProvider.Name, out isNew), + _ => GetItemById<Folder>(info.Id, channelProvider.Name, out isNew) + }; } else if (info.MediaType == ChannelMediaType.Audio) { - if (info.ContentType == ChannelMediaContentType.Podcast) - { - item = GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew); - } - else - { - item = GetItemById<Audio>(info.Id, channelProvider.Name, out isNew); - } + item = info.ContentType == ChannelMediaContentType.Podcast + ? GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew) + : GetItemById<Audio>(info.Id, channelProvider.Name, out isNew); } else { - if (info.ContentType == ChannelMediaContentType.Episode) - { - item = GetItemById<Episode>(info.Id, channelProvider.Name, out isNew); - } - else if (info.ContentType == ChannelMediaContentType.Movie) - { - item = GetItemById<Movie>(info.Id, channelProvider.Name, out isNew); - } - else if (info.ContentType == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer) + item = info.ContentType switch { - item = GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew); - } - else - { - item = GetItemById<Video>(info.Id, channelProvider.Name, out isNew); - } + ChannelMediaContentType.Episode => GetItemById<Episode>(info.Id, channelProvider.Name, out isNew), + ChannelMediaContentType.Movie => GetItemById<Movie>(info.Id, channelProvider.Name, out isNew), + var x when x == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer + => GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew), + _ => GetItemById<Video>(info.Id, channelProvider.Name, out isNew) + }; } var enableMediaProbe = channelProvider is ISupportsMediaProbe; @@ -981,7 +1006,6 @@ namespace Emby.Server.Implementations.Channels { item.RunTimeTicks = null; } - else if (isNew || !enableMediaProbe) { item.RunTimeTicks = info.RunTimeTicks; @@ -1014,26 +1038,24 @@ namespace Emby.Server.Implementations.Channels } } - var hasArtists = item as IHasArtist; - if (hasArtists != null) + if (item is IHasArtist hasArtists) { hasArtists.Artists = info.Artists.ToArray(); } - var hasAlbumArtists = item as IHasAlbumArtist; - if (hasAlbumArtists != null) + if (item is IHasAlbumArtist hasAlbumArtists) { hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray(); } - var trailer = item as Trailer; - if (trailer != null) + if (item is Trailer trailer) { if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes)) { _logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name); forceUpdate = true; } + trailer.TrailerTypes = info.TrailerTypes.ToArray(); } @@ -1057,6 +1079,7 @@ namespace Emby.Server.Implementations.Channels forceUpdate = true; _logger.LogDebug("Forcing update due to ChannelId {0}", item.Name); } + item.ChannelId = internalChannelId; if (!item.ParentId.Equals(parentFolderId)) @@ -1064,16 +1087,17 @@ namespace Emby.Server.Implementations.Channels forceUpdate = true; _logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name); } + item.ParentId = parentFolderId; - var hasSeries = item as IHasSeries; - if (hasSeries != null) + if (item is IHasSeries hasSeries) { if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase)) { forceUpdate = true; _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name); } + hasSeries.SeriesName = info.SeriesName; } @@ -1082,24 +1106,23 @@ namespace Emby.Server.Implementations.Channels forceUpdate = true; _logger.LogDebug("Forcing update due to ExternalId {0}", item.Name); } + item.ExternalId = info.Id; - var channelAudioItem = item as Audio; - if (channelAudioItem != null) + if (item is Audio channelAudioItem) { channelAudioItem.ExtraType = info.ExtraType; var mediaSource = info.MediaSources.FirstOrDefault(); - item.Path = mediaSource == null ? null : mediaSource.Path; + item.Path = mediaSource?.Path; } - var channelVideoItem = item as Video; - if (channelVideoItem != null) + if (item is Video channelVideoItem) { channelVideoItem.ExtraType = info.ExtraType; var mediaSource = info.MediaSources.FirstOrDefault(); - item.Path = mediaSource == null ? null : mediaSource.Path; + item.Path = mediaSource?.Path; } if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary)) @@ -1156,7 +1179,7 @@ namespace Emby.Server.Implementations.Channels } } - if (isNew || forceUpdate || item.DateLastRefreshed == default(DateTime)) + if (isNew || forceUpdate || item.DateLastRefreshed == default) { _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index 266d539d0a..eeb49b8fef 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Linq; using System.Threading; @@ -11,21 +9,34 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Channels { + /// <summary> + /// A task to remove all non-installed channels from the database. + /// </summary> public class ChannelPostScanTask { private readonly IChannelManager _channelManager; - private readonly IUserManager _userManager; private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; - public ChannelPostScanTask(IChannelManager channelManager, IUserManager userManager, ILogger logger, ILibraryManager libraryManager) + /// <summary> + /// Initializes a new instance of the <see cref="ChannelPostScanTask"/> class. + /// </summary> + /// <param name="channelManager">The channel manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="libraryManager">The library manager.</param> + public ChannelPostScanTask(IChannelManager channelManager, ILogger logger, ILibraryManager libraryManager) { _channelManager = channelManager; - _userManager = userManager; _logger = logger; _libraryManager = libraryManager; } + /// <summary> + /// Runs this task. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The completed task.</returns> public Task Run(IProgress<double> progress, CancellationToken cancellationToken) { CleanDatabase(cancellationToken); diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs index 367efcb134..54b621e250 100644 --- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Threading; @@ -7,29 +5,36 @@ using System.Threading.Tasks; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.Channels { + /// <summary> + /// The "Refresh Channels" scheduled task. + /// </summary> public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask { private readonly IChannelManager _channelManager; - private readonly IUserManager _userManager; private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; + /// <summary> + /// Initializes a new instance of the <see cref="RefreshChannelsScheduledTask"/> class. + /// </summary> + /// <param name="channelManager">The channel manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="localization">The localization manager.</param> public RefreshChannelsScheduledTask( IChannelManager channelManager, - IUserManager userManager, ILogger<RefreshChannelsScheduledTask> logger, ILibraryManager libraryManager, ILocalizationManager localization) { _channelManager = channelManager; - _userManager = userManager; _logger = logger; _libraryManager = libraryManager; _localization = localization; @@ -63,7 +68,7 @@ namespace Emby.Server.Implementations.Channels await manager.RefreshChannels(new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); - await new ChannelPostScanTask(_channelManager, _userManager, _logger, _libraryManager).Run(progress, cancellationToken) + await new ChannelPostScanTask(_channelManager, _logger, _libraryManager).Run(progress, cancellationToken) .ConfigureAwait(false); } @@ -72,7 +77,6 @@ namespace Emby.Server.Implementations.Channels { return new[] { - // Every so often new TaskTriggerInfo { diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index 21ba0288ec..c69a07e83e 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using System.Linq; using Emby.Server.Implementations.Images; @@ -15,8 +13,18 @@ using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.Collections { + /// <summary> + /// A collection image provider. + /// </summary> public class CollectionImageProvider : BaseDynamicImageProvider<BoxSet> { + /// <summary> + /// Initializes a new instance of the <see cref="CollectionImageProvider"/> class. + /// </summary> + /// <param name="fileSystem">The filesystem.</param> + /// <param name="providerManager">The provider manager.</param> + /// <param name="applicationPaths">The application paths.</param> + /// <param name="imageProcessor">The image processor.</param> public CollectionImageProvider( IFileSystem fileSystem, IProviderManager providerManager, @@ -26,6 +34,7 @@ namespace Emby.Server.Implementations.Collections { } + /// <inheritdoc /> protected override bool Supports(BaseItem item) { // Right now this is the only way to prevent this image from getting created ahead of internet image providers @@ -37,6 +46,7 @@ namespace Emby.Server.Implementations.Collections return base.Supports(item); } + /// <inheritdoc /> protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item) { var playlist = (BoxSet)item; @@ -48,13 +58,10 @@ namespace Emby.Server.Implementations.Collections var episode = subItem as Episode; - if (episode != null) + var series = episode?.Series; + if (series != null && series.HasImage(ImageType.Primary)) { - var series = episode.Series; - if (series != null && series.HasImage(ImageType.Primary)) - { - return series; - } + return series; } if (subItem.HasImage(ImageType.Primary)) @@ -80,6 +87,7 @@ namespace Emby.Server.Implementations.Collections .ToList(); } + /// <inheritdoc /> protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 3219528749..7c518d4831 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -23,6 +21,9 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Collections { + /// <summary> + /// The collection manager. + /// </summary> public class CollectionManager : ICollectionManager { private readonly ILibraryManager _libraryManager; @@ -33,6 +34,16 @@ namespace Emby.Server.Implementations.Collections private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; + /// <summary> + /// Initializes a new instance of the <see cref="CollectionManager"/> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="appPaths">The application paths.</param> + /// <param name="localizationManager">The localization manager.</param> + /// <param name="fileSystem">The filesystem.</param> + /// <param name="iLibraryMonitor">The library monitor.</param> + /// <param name="loggerFactory">The logger factory.</param> + /// <param name="providerManager">The provider manager.</param> public CollectionManager( ILibraryManager libraryManager, IApplicationPaths appPaths, @@ -51,8 +62,13 @@ namespace Emby.Server.Implementations.Collections _appPaths = appPaths; } + /// <inheritdoc /> public event EventHandler<CollectionCreatedEventArgs> CollectionCreated; + + /// <inheritdoc /> public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection; + + /// <inheritdoc /> public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection; private IEnumerable<Folder> FindFolders(string path) @@ -109,11 +125,12 @@ namespace Emby.Server.Implementations.Collections { var folder = GetCollectionsFolder(false).Result; - return folder == null ? - new List<BoxSet>() : - folder.GetChildren(user, true).OfType<BoxSet>(); + return folder == null + ? Enumerable.Empty<BoxSet>() + : folder.GetChildren(user, true).OfType<BoxSet>(); } + /// <inheritdoc /> public BoxSet CreateCollection(CollectionCreationOptions options) { var name = options.Name; @@ -178,11 +195,13 @@ namespace Emby.Server.Implementations.Collections } } + /// <inheritdoc /> public void AddToCollection(Guid collectionId, IEnumerable<string> ids) { AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); } + /// <inheritdoc /> public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids) { AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); @@ -191,7 +210,6 @@ namespace Emby.Server.Implementations.Collections private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; - if (collection == null) { throw new ArgumentException("No collection exists with the supplied Id"); @@ -246,11 +264,13 @@ namespace Emby.Server.Implementations.Collections } } + /// <inheritdoc /> public void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds) { RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i))); } + /// <inheritdoc /> public void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; @@ -289,10 +309,13 @@ namespace Emby.Server.Implementations.Collections } collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ForceSave = true - }, RefreshPriority.High); + _providerManager.QueueRefresh( + collection.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + RefreshPriority.High); ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs { @@ -301,6 +324,7 @@ namespace Emby.Server.Implementations.Collections }); } + /// <inheritdoc /> public IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user) { var results = new Dictionary<Guid, BaseItem>(); @@ -309,9 +333,7 @@ namespace Emby.Server.Implementations.Collections foreach (var item in items) { - var grouping = item as ISupportsBoxSetGrouping; - - if (grouping == null) + if (!(item is ISupportsBoxSetGrouping)) { results[item.Id] = item; } @@ -341,12 +363,21 @@ namespace Emby.Server.Implementations.Collections } } + /// <summary> + /// The collection manager entry point. + /// </summary> public sealed class CollectionManagerEntryPoint : IServerEntryPoint { private readonly CollectionManager _collectionManager; private readonly IServerConfigurationManager _config; private readonly ILogger _logger; + /// <summary> + /// Initializes a new instance of the <see cref="CollectionManagerEntryPoint"/> class. + /// </summary> + /// <param name="collectionManager">The collection manager.</param> + /// <param name="config">The server configuration manager.</param> + /// <param name="logger">The logger.</param> public CollectionManagerEntryPoint( ICollectionManager collectionManager, IServerConfigurationManager config, diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index f407317ec7..305e67e8c3 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -67,23 +67,22 @@ namespace Emby.Server.Implementations.Configuration /// <summary> /// Updates the metadata path. /// </summary> + /// <exception cref="UnauthorizedAccessException">If the directory does not exist, and the caller does not have the required permission to create it.</exception> + /// <exception cref="NotSupportedException">If there is a custom path transcoding path specified, but it is invalid.</exception> + /// <exception cref="IOException">If the directory does not exist, and it also could not be created.</exception> private void UpdateMetadataPath() { - if (string.IsNullOrWhiteSpace(Configuration.MetadataPath)) - { - ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Path.Combine(ApplicationPaths.ProgramDataPath, "metadata"); - } - else - { - ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Configuration.MetadataPath; - } + ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = string.IsNullOrWhiteSpace(Configuration.MetadataPath) + ? ApplicationPaths.DefaultInternalMetadataPath + : Configuration.MetadataPath; + Directory.CreateDirectory(ApplicationPaths.InternalMetadataPath); } /// <summary> /// Replaces the configuration. /// </summary> /// <param name="newConfiguration">The new configuration.</param> - /// <exception cref="DirectoryNotFoundException"></exception> + /// <exception cref="DirectoryNotFoundException">If the configuration path doesn't exist.</exception> public override void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration) { var newConfig = (ServerConfiguration)newConfiguration; @@ -194,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration changed = true; } - if (!config.CameraUploadUpgraded) - { - config.CameraUploadUpgraded = true; - changed = true; - } - if (!config.CollectionsUpgraded) { config.CollectionsUpgraded = true; diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 20bdd18e70..dea9b6682a 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.Updates; -using MediaBrowser.Providers.Music; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations @@ -18,7 +17,7 @@ namespace Emby.Server.Implementations { { HostWebClientKey, bool.TrueString }, { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, - { InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest.json" }, + { InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest-stable.json" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, { PlaylistsAllowDuplicatesKey, bool.TrueString } diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index de83b023d7..a037415a95 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.Cryptography private RandomNumberGenerator _randomNumberGenerator; - private bool _disposed = false; + private bool _disposed; /// <summary> /// Initializes a new instance of the <see cref="CryptographyProvider"/> class. @@ -56,15 +56,13 @@ namespace Emby.Server.Implementations.Cryptography { // downgrading for now as we need this library to be dotnetstandard compliant // with this downgrade we'll add a check to make sure we're on the downgrade method at the moment - if (method == DefaultHashMethod) + if (method != DefaultHashMethod) { - using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) - { - return r.GetBytes(32); - } + throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); } - throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); + using var r = new Rfc2898DeriveBytes(bytes, salt, iterations); + return r.GetBytes(32); } /// <inheritdoc /> @@ -74,25 +72,22 @@ namespace Emby.Server.Implementations.Cryptography { return PBKDF2(hashMethod, bytes, salt, DefaultIterations); } - else if (_supportedHashMethods.Contains(hashMethod)) + + if (!_supportedHashMethods.Contains(hashMethod)) + { + throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); + } + + using var h = HashAlgorithm.Create(hashMethod); + if (salt.Length == 0) { - using (var h = HashAlgorithm.Create(hashMethod)) - { - if (salt.Length == 0) - { - return h.ComputeHash(bytes); - } - else - { - byte[] salted = new byte[bytes.Length + salt.Length]; - Array.Copy(bytes, salted, bytes.Length); - Array.Copy(salt, 0, salted, bytes.Length, salt.Length); - return h.ComputeHash(salted); - } - } + return h.ComputeHash(bytes); } - throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); + byte[] salted = new byte[bytes.Length + salt.Length]; + Array.Copy(bytes, salted, bytes.Length); + Array.Copy(salt, 0, salted, bytes.Length, salt.Length); + return h.ComputeHash(salted); } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index e7c5270b9f..716e5071d5 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using MediaBrowser.Model.Serialization; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data @@ -109,25 +107,6 @@ namespace Emby.Server.Implementations.Data return null; } - /// <summary> - /// Serializes to bytes. - /// </summary> - /// <returns>System.Byte[][].</returns> - /// <exception cref="ArgumentNullException">obj</exception> - public static byte[] SerializeToBytes(this IJsonSerializer json, object obj) - { - if (obj == null) - { - throw new ArgumentNullException(nameof(obj)); - } - - using (var stream = new MemoryStream()) - { - json.SerializeToStream(obj, stream); - return stream.ToArray(); - } - } - public static void Attach(SQLiteDatabaseConnection db, string path, string alias) { var commandText = string.Format( @@ -383,11 +362,11 @@ namespace Emby.Server.Implementations.Data } } - public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement This) + public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement) { - while (This.MoveNext()) + while (statement.MoveNext()) { - yield return This.Current; + yield return statement.Current; } } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 33ff74bb58..ca5cd6fdd5 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -39,12 +39,11 @@ namespace Emby.Server.Implementations.Data { private const string ChaptersTableName = "Chapters2"; - /// <summary> - /// The _app paths - /// </summary> private readonly IServerConfigurationManager _config; private readonly IServerApplicationHost _appHost; private readonly ILocalizationManager _localization; + // TODO: Remove this dependency. GetImageCacheTag() is the only method used and it can be converted to a static helper method + private readonly IImageProcessor _imageProcessor; private readonly TypeMapper _typeMapper; private readonly JsonSerializerOptions _jsonOptions; @@ -71,7 +70,8 @@ namespace Emby.Server.Implementations.Data IServerConfigurationManager config, IServerApplicationHost appHost, ILogger<SqliteItemRepository> logger, - ILocalizationManager localization) + ILocalizationManager localization, + IImageProcessor imageProcessor) : base(logger) { if (config == null) @@ -82,6 +82,7 @@ namespace Emby.Server.Implementations.Data _config = config; _appHost = appHost; _localization = localization; + _imageProcessor = imageProcessor; _typeMapper = new TypeMapper(); _jsonOptions = JsonDefaults.GetOptions(); @@ -98,8 +99,6 @@ namespace Emby.Server.Implementations.Data /// <inheritdoc /> protected override TempStoreMode TempStore => TempStoreMode.Memory; - public IImageProcessor ImageProcessor { get; set; } - /// <summary> /// Opens the connection to the database /// </summary> @@ -1991,7 +1990,14 @@ namespace Emby.Server.Implementations.Data if (!string.IsNullOrEmpty(chapter.ImagePath)) { - chapter.ImageTag = ImageProcessor.GetImageCacheTag(item, chapter); + try + { + chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to create image cache tag."); + } } } diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 22955850ab..6ee6230fc6 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -375,5 +375,15 @@ namespace Emby.Server.Implementations.Data return userData; } + + /// <inheritdoc/> + /// <remarks> + /// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and + /// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>. + /// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>. + /// </remarks> + protected override void Dispose(bool dispose) + { + } } } diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index adb8e793d2..2283f2433a 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -5,27 +5,18 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Devices { @@ -33,42 +24,27 @@ namespace Emby.Server.Implementations.Devices { private readonly IJsonSerializer _json; private readonly IUserManager _userManager; - private readonly IFileSystem _fileSystem; - private readonly ILibraryMonitor _libraryMonitor; private readonly IServerConfigurationManager _config; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localizationManager; - private readonly IAuthenticationRepository _authRepo; + private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache; public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; - public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; - private readonly object _cameraUploadSyncLock = new object(); private readonly object _capabilitiesSyncLock = new object(); public DeviceManager( IAuthenticationRepository authRepo, IJsonSerializer json, - ILibraryManager libraryManager, - ILocalizationManager localizationManager, IUserManager userManager, - IFileSystem fileSystem, - ILibraryMonitor libraryMonitor, IServerConfigurationManager config) { _json = json; _userManager = userManager; - _fileSystem = fileSystem; - _libraryMonitor = libraryMonitor; _config = config; - _libraryManager = libraryManager; - _localizationManager = localizationManager; _authRepo = authRepo; + _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase); } - - private Dictionary<string, ClientCapabilities> _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase); public void SaveCapabilities(string deviceId, ClientCapabilities capabilities) { var path = Path.Combine(GetDevicePath(deviceId), "capabilities.json"); @@ -194,172 +170,6 @@ namespace Emby.Server.Implementations.Devices return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture)); } - public ContentUploadHistory GetCameraUploadHistory(string deviceId) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - - lock (_cameraUploadSyncLock) - { - try - { - return _json.DeserializeFromFile<ContentUploadHistory>(path); - } - catch (IOException) - { - return new ContentUploadHistory - { - DeviceId = deviceId - }; - } - } - } - - public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file) - { - var device = GetDevice(deviceId, false); - var uploadPathInfo = GetUploadPath(device); - - var path = uploadPathInfo.Item1; - - if (!string.IsNullOrWhiteSpace(file.Album)) - { - path = Path.Combine(path, _fileSystem.GetValidFilename(file.Album)); - } - - path = Path.Combine(path, file.Name); - path = Path.ChangeExtension(path, MimeTypes.ToExtension(file.MimeType) ?? "jpg"); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - await EnsureLibraryFolder(uploadPathInfo.Item2, uploadPathInfo.Item3).ConfigureAwait(false); - - _libraryMonitor.ReportFileSystemChangeBeginning(path); - - try - { - using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - await stream.CopyToAsync(fs).ConfigureAwait(false); - } - - AddCameraUpload(deviceId, file); - } - finally - { - _libraryMonitor.ReportFileSystemChangeComplete(path, true); - } - - if (CameraImageUploaded != null) - { - CameraImageUploaded?.Invoke(this, new GenericEventArgs<CameraImageUploadInfo> - { - Argument = new CameraImageUploadInfo - { - Device = device, - FileInfo = file - } - }); - } - } - - private void AddCameraUpload(string deviceId, LocalFileInfo file) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_cameraUploadSyncLock) - { - ContentUploadHistory history; - - try - { - history = _json.DeserializeFromFile<ContentUploadHistory>(path); - } - catch (IOException) - { - history = new ContentUploadHistory - { - DeviceId = deviceId - }; - } - - history.DeviceId = deviceId; - - var list = history.FilesUploaded.ToList(); - list.Add(file); - history.FilesUploaded = list.ToArray(); - - _json.SerializeToFile(history, path); - } - } - - internal Task EnsureLibraryFolder(string path, string name) - { - var existingFolders = _libraryManager - .RootFolder - .Children - .OfType<Folder>() - .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path)) - .ToList(); - - if (existingFolders.Count > 0) - { - return Task.CompletedTask; - } - - Directory.CreateDirectory(path); - - var libraryOptions = new LibraryOptions - { - PathInfos = new[] { new MediaPathInfo { Path = path } }, - EnablePhotos = true, - EnableRealtimeMonitor = false, - SaveLocalMetadata = true - }; - - if (string.IsNullOrWhiteSpace(name)) - { - name = _localizationManager.GetLocalizedString("HeaderCameraUploads"); - } - - return _libraryManager.AddVirtualFolder(name, CollectionType.HomeVideos, libraryOptions, true); - } - - private Tuple<string, string, string> GetUploadPath(DeviceInfo device) - { - var config = _config.GetUploadOptions(); - var path = config.CameraUploadPath; - - if (string.IsNullOrWhiteSpace(path)) - { - path = DefaultCameraUploadsPath; - } - - var topLibraryPath = path; - - if (config.EnableCameraUploadSubfolders) - { - path = Path.Combine(path, _fileSystem.GetValidFilename(device.Name)); - } - - return new Tuple<string, string, string>(path, topLibraryPath, null); - } - - internal string GetUploadsPath() - { - var config = _config.GetUploadOptions(); - var path = config.CameraUploadPath; - - if (string.IsNullOrWhiteSpace(path)) - { - path = DefaultCameraUploadsPath; - } - - return path; - } - - private string DefaultCameraUploadsPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); - public bool CanAccessDevice(User user, string deviceId) { if (user == null) @@ -399,102 +209,4 @@ namespace Emby.Server.Implementations.Devices return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase); } } - - public class DeviceManagerEntryPoint : IServerEntryPoint - { - private readonly DeviceManager _deviceManager; - private readonly IServerConfigurationManager _config; - private ILogger _logger; - - public DeviceManagerEntryPoint( - IDeviceManager deviceManager, - IServerConfigurationManager config, - ILogger<DeviceManagerEntryPoint> logger) - { - _deviceManager = (DeviceManager)deviceManager; - _config = config; - _logger = logger; - } - - public async Task RunAsync() - { - if (!_config.Configuration.CameraUploadUpgraded && _config.Configuration.IsStartupWizardCompleted) - { - var path = _deviceManager.GetUploadsPath(); - - if (Directory.Exists(path)) - { - try - { - await _deviceManager.EnsureLibraryFolder(path, null).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating camera uploads library"); - } - - _config.Configuration.CameraUploadUpgraded = true; - _config.SaveConfiguration(); - } - } - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects). - } - - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. - // TODO: set large fields to null. - - disposedValue = true; - } - } - - // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. - // ~DeviceManagerEntryPoint() { - // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - // Dispose(false); - // } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - // TODO: uncomment the following line if the finalizer is overridden above. - // GC.SuppressFinalize(this); - } - #endregion - } - - public class DevicesConfigStore : IConfigurationFactory - { - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new ConfigurationStore[] - { - new ConfigurationStore - { - Key = "devices", - ConfigurationType = typeof(DevicesOptions) - } - }; - } - } - - public static class UploadConfigExtension - { - public static DevicesOptions GetUploadOptions(this IConfigurationManager config) - { - return config.GetConfiguration<DevicesOptions>("devices"); - } - } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 34a342cf7e..c4b65d2654 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -38,21 +38,23 @@ namespace Emby.Server.Implementations.Dto private readonly IProviderManager _providerManager; private readonly IApplicationHost _appHost; - private readonly Func<IMediaSourceManager> _mediaSourceManager; - private readonly Func<ILiveTvManager> _livetvManager; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly Lazy<ILiveTvManager> _livetvManagerFactory; + + private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; public DtoService( - ILoggerFactory loggerFactory, + ILogger<DtoService> logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IImageProcessor imageProcessor, IProviderManager providerManager, IApplicationHost appHost, - Func<IMediaSourceManager> mediaSourceManager, - Func<ILiveTvManager> livetvManager) + IMediaSourceManager mediaSourceManager, + Lazy<ILiveTvManager> livetvManagerFactory) { - _logger = loggerFactory.CreateLogger(nameof(DtoService)); + _logger = logger; _libraryManager = libraryManager; _userDataRepository = userDataRepository; _itemRepo = itemRepo; @@ -60,7 +62,7 @@ namespace Emby.Server.Implementations.Dto _providerManager = providerManager; _appHost = appHost; _mediaSourceManager = mediaSourceManager; - _livetvManager = livetvManager; + _livetvManagerFactory = livetvManagerFactory; } /// <summary> @@ -125,12 +127,12 @@ namespace Emby.Server.Implementations.Dto if (programTuples.Count > 0) { - _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult(); + LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult(); } if (channelTuples.Count > 0) { - _livetvManager().AddChannelInfo(channelTuples, options, user); + LivetvManager.AddChannelInfo(channelTuples, options, user); } return returnItems; @@ -142,12 +144,12 @@ namespace Emby.Server.Implementations.Dto if (item is LiveTvChannel tvChannel) { var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) }; - _livetvManager().AddChannelInfo(list, options, user); + LivetvManager.AddChannelInfo(list, options, user); } else if (item is LiveTvProgram) { var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) }; - var task = _livetvManager().AddInfoToProgramDto(list, options.Fields, user); + var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user); Task.WaitAll(task); } @@ -223,7 +225,7 @@ namespace Emby.Server.Implementations.Dto if (item is IHasMediaSources && options.ContainsField(ItemFields.MediaSources)) { - dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray(); + dto.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray(); NormalizeMediaSourceContainers(dto); } @@ -254,7 +256,7 @@ namespace Emby.Server.Implementations.Dto dto.Etag = item.GetEtag(user); } - var liveTvManager = _livetvManager(); + var liveTvManager = LivetvManager; var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path); if (activeRecording != null) { @@ -1045,7 +1047,7 @@ namespace Emby.Server.Implementations.Dto } else { - mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true)[0].MediaStreams.ToArray(); + mediaStreams = _mediaSourceManager.GetStaticMediaSources(item, true)[0].MediaStreams.ToArray(); } dto.MediaStreams = mediaStreams; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 3d065f70a9..b69a126b3a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -1,4 +1,9 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> + + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{E383961B-9356-4D5D-8233-9A1079D03055}</ProjectGuid> + </PropertyGroup> <ItemGroup> <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" /> @@ -29,14 +34,16 @@ <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.3" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> - <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.4" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" /> <PackageReference Include="Mono.Nat" Version="2.0.1" /> + <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" /> <PackageReference Include="sharpcompress" Version="0.25.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> + <PackageReference Include="DotNet.Glob" Version="3.0.9" /> </ItemGroup> <ItemGroup> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index e290c62e16..878cee23c4 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Text; @@ -26,10 +27,10 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IServerConfigurationManager _config; private readonly IDeviceDiscovery _deviceDiscovery; - private readonly object _createdRulesLock = new object(); - private List<IPEndPoint> _createdRules = new List<IPEndPoint>(); + private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>(); + private Timer _timer; - private string _lastConfigIdentifier; + private string _configIdentifier; private bool _disposed = false; @@ -60,16 +61,20 @@ namespace Emby.Server.Implementations.EntryPoints return new StringBuilder(32) .Append(config.EnableUPnP).Append(Separator) .Append(config.PublicPort).Append(Separator) + .Append(config.PublicHttpsPort).Append(Separator) .Append(_appHost.HttpPort).Append(Separator) .Append(_appHost.HttpsPort).Append(Separator) - .Append(_appHost.EnableHttps).Append(Separator) + .Append(_appHost.ListenWithHttps).Append(Separator) .Append(config.EnableRemoteAccess).Append(Separator) .ToString(); } private void OnConfigurationUpdated(object sender, EventArgs e) { - if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase)) + var oldConfigIdentifier = _configIdentifier; + _configIdentifier = GetConfigIdentifier(); + + if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase)) { Stop(); Start(); @@ -93,21 +98,19 @@ namespace Emby.Server.Implementations.EntryPoints return; } - _logger.LogDebug("Starting NAT discovery"); + _logger.LogInformation("Starting NAT discovery"); NatUtility.DeviceFound += OnNatUtilityDeviceFound; NatUtility.StartDiscovery(); - _timer = new Timer(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); + _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; - - _lastConfigIdentifier = GetConfigIdentifier(); } private void Stop() { - _logger.LogDebug("Stopping NAT discovery"); + _logger.LogInformation("Stopping NAT discovery"); NatUtility.StopDiscovery(); NatUtility.DeviceFound -= OnNatUtilityDeviceFound; @@ -117,26 +120,16 @@ namespace Emby.Server.Implementations.EntryPoints _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; } - private void ClearCreatedRules(object state) - { - lock (_createdRulesLock) - { - _createdRules.Clear(); - } - } - private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) { NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp); } - private void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) + private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) { try { - var device = e.Device; - - CreateRules(device); + await CreateRules(e.Device).ConfigureAwait(false); } catch (Exception ex) { @@ -144,7 +137,7 @@ namespace Emby.Server.Implementations.EntryPoints } } - private async void CreateRules(INatDevice device) + private Task CreateRules(INatDevice device) { if (_disposed) { @@ -153,50 +146,46 @@ namespace Emby.Server.Implementations.EntryPoints // On some systems the device discovered event seems to fire repeatedly // This check will help ensure we're not trying to port map the same device over and over - var address = device.DeviceEndpoint; - - lock (_createdRulesLock) + if (!_createdRules.TryAdd(device.DeviceEndpoint, 0)) { - if (!_createdRules.Contains(address)) - { - _createdRules.Add(address); - } - else - { - return; - } + return Task.CompletedTask; } - try - { - await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating http port map"); - return; - } + return Task.WhenAll(CreatePortMaps(device)); + } - try - { - await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false); - } - catch (Exception ex) + private IEnumerable<Task> CreatePortMaps(INatDevice device) + { + yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort); + + if (_appHost.ListenWithHttps) { - _logger.LogError(ex, "Error creating https port map"); + yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort); } } - private Task<Mapping> CreatePortMap(INatDevice device, int privatePort, int publicPort) + private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort) { _logger.LogDebug( - "Creating port map on local port {0} to public port {1} with device {2}", + "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}", privatePort, publicPort, device.DeviceEndpoint); - return device.CreatePortMapAsync( - new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name)); + try + { + var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name); + await device.CreatePortMapAsync(mapping).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.", + privatePort, + publicPort, + device.DeviceEndpoint); + } } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index 8e97719311..2e738deeb5 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -16,46 +16,63 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IServerApplicationHost _appHost; private readonly IConfiguration _appConfig; private readonly IServerConfigurationManager _config; + private readonly IStartupOptions _startupOptions; /// <summary> /// Initializes a new instance of the <see cref="StartupWizard"/> class. /// </summary> /// <param name="appHost">The application host.</param> + /// <param name="appConfig">The application configuration.</param> /// <param name="config">The configuration manager.</param> - public StartupWizard(IServerApplicationHost appHost, IConfiguration appConfig, IServerConfigurationManager config) + /// <param name="startupOptions">The application startup options.</param> + public StartupWizard( + IServerApplicationHost appHost, + IConfiguration appConfig, + IServerConfigurationManager config, + IStartupOptions startupOptions) { _appHost = appHost; _appConfig = appConfig; _config = config; + _startupOptions = startupOptions; } /// <inheritdoc /> public Task RunAsync() + { + Run(); + return Task.CompletedTask; + } + + private void Run() { if (!_appHost.CanLaunchWebBrowser) { - return Task.CompletedTask; + return; } - if (!_appConfig.HostWebClient()) + // Always launch the startup wizard if possible when it has not been completed + if (!_config.Configuration.IsStartupWizardCompleted && _appConfig.HostWebClient()) { - BrowserLauncher.OpenSwaggerPage(_appHost); + BrowserLauncher.OpenWebApp(_appHost); + return; + } + + // Do nothing if the web app is configured to not run automatically + if (!_config.Configuration.AutoRunWebApp || _startupOptions.NoAutoRunWebApp) + { + return; } - else if (!_config.Configuration.IsStartupWizardCompleted) + + // Launch the swagger page if the web client is not hosted, otherwise open the web client + if (_appConfig.HostWebClient()) { BrowserLauncher.OpenWebApp(_appHost); } - else if (_config.Configuration.AutoRunWebApp) + else { - var options = ((ApplicationHost)_appHost).StartupOptions; - - if (!options.NoAutoRunWebApp) - { - BrowserLauncher.OpenWebApp(_appHost); - } + BrowserLauncher.OpenSwaggerPage(_appHost); } - - return Task.CompletedTask; } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 50ba0f8fac..6929c81f92 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Udp; using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints @@ -22,6 +23,7 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _config; /// <summary> /// The UDP server. @@ -35,18 +37,19 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> public UdpServerEntryPoint( ILogger<UdpServerEntryPoint> logger, - IServerApplicationHost appHost) + IServerApplicationHost appHost, + IConfiguration configuration) { _logger = logger; _appHost = appHost; - + _config = configuration; } /// <inheritdoc /> public async Task RunAsync() { - _udpServer = new UdpServer(_logger, _appHost); + _udpServer = new UdpServer(_logger, _appHost, _config); _udpServer.Start(PortNumber, _cancellationTokenSource.Token); } diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 882bfe2f6e..d66bb7638b 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -24,7 +25,7 @@ namespace Emby.Server.Implementations.HttpClientManager private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; - private readonly Func<string> _defaultUserAgentFn; + private readonly IApplicationHost _appHost; /// <summary> /// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests. @@ -40,12 +41,12 @@ namespace Emby.Server.Implementations.HttpClientManager IApplicationPaths appPaths, ILogger<HttpClientManager> logger, IFileSystem fileSystem, - Func<string> defaultUserAgentFn) + IApplicationHost appHost) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _fileSystem = fileSystem; _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths)); - _defaultUserAgentFn = defaultUserAgentFn; + _appHost = appHost; } /// <summary> @@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.HttpClientManager if (options.EnableDefaultUserAgent && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _)) { - request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn()); + request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent); } switch (options.DecompressionMethod) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 5ae65a4e3d..7de4f168c1 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -6,14 +6,16 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Sockets; +using System.Net.WebSockets; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Services; +using Emby.Server.Implementations.SocketSharp; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Events; @@ -21,15 +23,17 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using ServiceStack.Text.Jsv; namespace Emby.Server.Implementations.HttpServer { - public class HttpListenerHost : IHttpServer, IDisposable + public class HttpListenerHost : IHttpServer { /// <summary> /// The key for a setting that specifies the default redirect path @@ -38,17 +42,17 @@ namespace Emby.Server.Implementations.HttpServer public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; private readonly IServerApplicationHost _appHost; private readonly IJsonSerializer _jsonSerializer; private readonly IXmlSerializer _xmlSerializer; - private readonly IHttpListener _socketListener; private readonly Func<Type, Func<string, object>> _funcParseFn; private readonly string _defaultRedirectPath; private readonly string _baseUrlPrefix; + private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>(); - private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>(); private readonly IHostEnvironment _hostEnvironment; private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); @@ -62,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer INetworkManager networkManager, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, - IHttpListener socketListener, ILocalizationManager localizationManager, ServiceController serviceController, - IHostEnvironment hostEnvironment) + IHostEnvironment hostEnvironment, + ILoggerFactory loggerFactory) { _appHost = applicationHost; _logger = logger; @@ -75,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer _networkManager = networkManager; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; - _socketListener = socketListener; ServiceController = serviceController; - - _socketListener.WebSocketConnected = OnWebSocketConnected; _hostEnvironment = hostEnvironment; + _loggerFactory = loggerFactory; _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); @@ -171,38 +173,6 @@ namespace Emby.Server.Implementations.HttpServer return attributes; } - private void OnWebSocketConnected(WebSocketConnectEventArgs e) - { - if (_disposed) - { - return; - } - - var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) - { - OnReceive = ProcessWebSocketMessageReceived, - Url = e.Url, - QueryString = e.QueryString - }; - - connection.Closed += OnConnectionClosed; - - lock (_webSocketConnections) - { - _webSocketConnections.Add(connection); - } - - WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); - } - - private void OnConnectionClosed(object sender, EventArgs e) - { - lock (_webSocketConnections) - { - _webSocketConnections.Remove((IWebSocketConnection)sender); - } - } - private static Exception GetActualException(Exception ex) { if (ex is AggregateException agg) @@ -230,7 +200,8 @@ namespace Emby.Server.Implementations.HttpServer switch (ex) { case ArgumentException _: return 400; - case SecurityException _: return 401; + case AuthenticationException _: return 401; + case SecurityException _: return 403; case DirectoryNotFoundException _: case FileNotFoundException _: case ResourceNotFoundException _: return 404; @@ -239,81 +210,44 @@ namespace Emby.Server.Implementations.HttpServer } } - private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, string urlToLog) + private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace) { - try + if (ignoreStackTrace) { - ex = GetActualException(ex); - - if (logExceptionStackTrace) - { - _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog); - } - else - { - _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); - } - - var httpRes = httpReq.Response; - - if (httpRes.HasStarted) - { - return; - } - - var statusCode = GetStatusCode(ex); - httpRes.StatusCode = statusCode; - - var errContent = NormalizeExceptionMessage(ex.Message); - httpRes.ContentType = "text/plain"; - httpRes.ContentLength = errContent.Length; - await httpRes.WriteAsync(errContent).ConfigureAwait(false); + _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); } - catch (Exception errorEx) + else { - _logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response). URL: {Url}", urlToLog); + _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog); } - } - private string NormalizeExceptionMessage(string msg) - { - if (msg == null) + var httpRes = httpReq.Response; + + if (httpRes.HasStarted) { - return string.Empty; + return; } - // Strip any information we don't want to reveal - - msg = msg.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase); - msg = msg.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase); + httpRes.StatusCode = statusCode; - return msg; + var errContent = NormalizeExceptionMessage(ex) ?? string.Empty; + httpRes.ContentType = "text/plain"; + httpRes.ContentLength = errContent.Length; + await httpRes.WriteAsync(errContent).ConfigureAwait(false); } - /// <summary> - /// Shut down the Web Service - /// </summary> - public void Stop() + private string NormalizeExceptionMessage(Exception ex) { - List<IWebSocketConnection> connections; - - lock (_webSocketConnections) + // Do not expose the exception message for AuthenticationException + if (ex is AuthenticationException) { - connections = _webSocketConnections.ToList(); - _webSocketConnections.Clear(); + return null; } - foreach (var connection in connections) - { - try - { - connection.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing connection"); - } - } + // Strip any information we don't want to reveal + return ex.Message + ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase); } public static string RemoveQueryStringByKey(string url, string key) @@ -425,33 +359,52 @@ namespace Emby.Server.Implementations.HttpServer return true; } + /// <summary> + /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required. + /// </summary> + /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns> private bool ValidateSsl(string remoteIp, string urlString) { - if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy) + if (_config.Configuration.RequireHttps + && _appHost.ListenWithHttps + && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase)) { - if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1) + // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected + if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 + || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) { - // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected - if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 - || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } + return true; + } - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } + if (!_networkManager.IsInLocalNetwork(remoteIp)) + { + return false; } } return true; } + /// <inheritdoc /> + public Task RequestHandler(HttpContext context) + { + if (context.WebSockets.IsWebSocketRequest) + { + return WebSocketRequestHandler(context); + } + + var request = context.Request; + var response = context.Response; + var localPath = context.Request.Path.ToString(); + + var req = new WebSocketSharpRequest(request, response, request.Path, _logger); + return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted); + } + /// <summary> /// Overridable method that can be used to implement a custom handler. /// </summary> - public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) + private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) { var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -494,9 +447,10 @@ namespace Emby.Server.Implementations.HttpServer if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { httpRes.StatusCode = 200; - httpRes.Headers.Add("Access-Control-Allow-Origin", "*"); - httpRes.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - httpRes.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); + foreach(var (key, value) in GetDefaultCorsHeaders(httpReq)) + { + httpRes.Headers.Add(key, value); + } httpRes.ContentType = "text/plain"; await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); return; @@ -536,22 +490,50 @@ namespace Emby.Server.Implementations.HttpServer throw new FileNotFoundException(); } } - catch (Exception ex) + catch (Exception requestEx) { - // Do not handle exceptions manually when in development mode - // The framework-defined development exception page will be returned instead - if (_hostEnvironment.IsDevelopment()) + try { - throw; + var requestInnerEx = GetActualException(requestEx); + var statusCode = GetStatusCode(requestInnerEx); + + foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) + { + if (!httpRes.Headers.ContainsKey(key)) + { + httpRes.Headers.Add(key, value); + } + } + + bool ignoreStackTrace = + requestInnerEx is SocketException + || requestInnerEx is IOException + || requestInnerEx is OperationCanceledException + || requestInnerEx is SecurityException + || requestInnerEx is AuthenticationException + || requestInnerEx is FileNotFoundException; + + // Do not handle 500 server exceptions manually when in development mode. + // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware. + // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored, + // because it will log the stack trace when it handles the exception. + if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment()) + { + throw; + } + + await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false); } + catch (Exception handlerException) + { + var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException); + _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog); - bool ignoreStackTrace = - ex is SocketException - || ex is IOException - || ex is OperationCanceledException - || ex is SecurityException - || ex is FileNotFoundException; - await ErrorHandler(ex, httpReq, !ignoreStackTrace, urlToLog).ConfigureAwait(false); + if (_hostEnvironment.IsDevelopment()) + { + throw aggregateEx; + } + } } finally { @@ -569,6 +551,68 @@ namespace Emby.Server.Implementations.HttpServer } } + private async Task WebSocketRequestHandler(HttpContext context) + { + if (_disposed) + { + return; + } + + try + { + _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); + + WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); + + var connection = new WebSocketConnection( + _loggerFactory.CreateLogger<WebSocketConnection>(), + webSocket, + context.Connection.RemoteIpAddress, + context.Request.Query) + { + OnReceive = ProcessWebSocketMessageReceived + }; + + WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); + + await connection.ProcessAsync().ConfigureAwait(false); + _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); + } + catch (Exception ex) // Otherwise ASP.Net will ignore the exception + { + _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 500; + } + } + } + + /// <summary> + /// Get the default CORS headers + /// </summary> + /// <param name="req"></param> + /// <returns></returns> + public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req) + { + var origin = req.Headers["Origin"]; + if (origin == StringValues.Empty) + { + origin = req.Headers["Host"]; + if (origin == StringValues.Empty) + { + origin = "*"; + } + } + + var headers = new Dictionary<string, string>(); + headers.Add("Access-Control-Allow-Origin", origin); + headers.Add("Access-Control-Allow-Credentials", "true"); + headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie"); + return headers; + } + // Entry point for HttpListener public ServiceHandler GetServiceHandler(IHttpRequest httpReq) { @@ -615,7 +659,7 @@ namespace Emby.Server.Implementations.HttpServer ResponseFilters = new Action<IRequest, HttpResponse, object>[] { - new ResponseFilter(_logger).FilterResponse + new ResponseFilter(this, _logger).FilterResponse }; } @@ -676,11 +720,6 @@ namespace Emby.Server.Implementations.HttpServer return _jsonSerializer.DeserializeFromStreamAsync(stream, type); } - public Task ProcessWebSocketRequest(HttpContext context) - { - return _socketListener.ProcessWebSocketRequest(context); - } - private string NormalizeEmbyRoutePath(string path) { _logger.LogDebug("Normalizing /emby route"); @@ -699,28 +738,6 @@ namespace Emby.Server.Implementations.HttpServer return _baseUrlPrefix + NormalizeUrlPath(path); } - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - Stop(); - } - - _disposed = true; - } - /// <summary> /// Processes the web socket message received. /// </summary> @@ -732,8 +749,6 @@ namespace Emby.Server.Implementations.HttpServer return Task.CompletedTask; } - _logger.LogDebug("Websocket message received: {0}", result.MessageType); - IEnumerable<Task> GetTasks() { foreach (var x in _webSocketListeners) diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index b42662420b..2e9ecc4ae6 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -28,6 +28,12 @@ namespace Emby.Server.Implementations.HttpServer /// </summary> public class HttpResultFactory : IHttpResultFactory { + // Last-Modified and If-Modified-Since must follow strict date format, + // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since + private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\""; + // We specifically use en-US culture because both day of week and month names require it + private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false); + /// <summary> /// The logger. /// </summary> @@ -420,7 +426,11 @@ namespace Emby.Server.Implementations.HttpServer if (!noCache) { - DateTime.TryParse(requestContext.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); + if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader)) + { + _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]); + return null; + } if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified)) { @@ -629,7 +639,7 @@ namespace Emby.Server.Implementations.HttpServer if (lastModifiedDate.HasValue) { - responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture); + responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture); } } diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs deleted file mode 100644 index 5015937256..0000000000 --- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs +++ /dev/null @@ -1,39 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Net; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace Emby.Server.Implementations.HttpServer -{ - public interface IHttpListener : IDisposable - { - /// <summary> - /// Gets or sets the error handler. - /// </summary> - /// <value>The error handler.</value> - Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; } - - /// <summary> - /// Gets or sets the request handler. - /// </summary> - /// <value>The request handler.</value> - Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; } - - /// <summary> - /// Gets or sets the web socket handler. - /// </summary> - /// <value>The web socket handler.</value> - Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; } - - /// <summary> - /// Stops this instance. - /// </summary> - Task Stop(); - - Task ProcessWebSocketRequest(HttpContext ctx); - } -} diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index 5e0466629d..85c3db9b20 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Text; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -13,14 +15,17 @@ namespace Emby.Server.Implementations.HttpServer /// </summary> public class ResponseFilter { + private readonly IHttpServer _server; private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="ResponseFilter"/> class. /// </summary> + /// <param name="server">The HTTP server.</param> /// <param name="logger">The logger.</param> - public ResponseFilter(ILogger logger) + public ResponseFilter(IHttpServer server, ILogger logger) { + _server = server; _logger = logger; } @@ -32,10 +37,16 @@ namespace Emby.Server.Implementations.HttpServer /// <param name="dto">The dto.</param> public void FilterResponse(IRequest req, HttpResponse res, object dto) { + foreach(var (key, value) in _server.GetDefaultCorsHeaders(req)) + { + res.Headers.Add(key, value); + } // Try to prevent compatibility view - res.Headers.Add("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); - res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - res.Headers.Add("Access-Control-Allow-Origin", "*"); + res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " + + "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " + + "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " + + "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " + + "X-Emby-Authorization"); if (dto is Exception exception) { @@ -82,6 +93,10 @@ namespace Emby.Server.Implementations.HttpServer { return null; } + else if (inString.Length == 0) + { + return inString; + } var newString = new StringBuilder(inString.Length); diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 58421aaf19..256b24924e 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -2,6 +2,7 @@ using System; using System.Linq; +using System.Security.Authentication; using Emby.Server.Implementations.SocketSharp; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -68,7 +69,7 @@ namespace Emby.Server.Implementations.HttpServer.Security if (user == null && auth.UserId != Guid.Empty) { - throw new SecurityException("User with Id " + auth.UserId + " not found"); + throw new AuthenticationException("User with Id " + auth.UserId + " not found"); } if (user != null) @@ -108,18 +109,12 @@ namespace Emby.Server.Implementations.HttpServer.Security { if (user.Policy.IsDisabled) { - throw new SecurityException("User account has been disabled.") - { - SecurityExceptionType = SecurityExceptionType.Unauthenticated - }; + throw new SecurityException("User account has been disabled."); } if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp)) { - throw new SecurityException("User account has been disabled.") - { - SecurityExceptionType = SecurityExceptionType.Unauthenticated - }; + throw new SecurityException("User account has been disabled."); } if (!user.Policy.IsAdministrator @@ -128,10 +123,7 @@ namespace Emby.Server.Implementations.HttpServer.Security { request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl"); - throw new SecurityException("This user account is not allowed access at this time.") - { - SecurityExceptionType = SecurityExceptionType.ParentalControl - }; + throw new SecurityException("This user account is not allowed access at this time."); } } @@ -190,10 +182,7 @@ namespace Emby.Server.Implementations.HttpServer.Security { if (user == null || !user.Policy.IsAdministrator) { - throw new SecurityException("User does not have admin access.") - { - SecurityExceptionType = SecurityExceptionType.Unauthenticated - }; + throw new SecurityException("User does not have admin access."); } } @@ -201,10 +190,7 @@ namespace Emby.Server.Implementations.HttpServer.Security { if (user == null || !user.Policy.EnableContentDeletion) { - throw new SecurityException("User does not have delete access.") - { - SecurityExceptionType = SecurityExceptionType.Unauthenticated - }; + throw new SecurityException("User does not have delete access."); } } @@ -212,10 +198,7 @@ namespace Emby.Server.Implementations.HttpServer.Security { if (user == null || !user.Policy.EnableContentDownloading) { - throw new SecurityException("User does not have download access.") - { - SecurityExceptionType = SecurityExceptionType.Unauthenticated - }; + throw new SecurityException("User does not have download access."); } } } @@ -230,14 +213,14 @@ namespace Emby.Server.Implementations.HttpServer.Security { if (string.IsNullOrEmpty(token)) { - throw new SecurityException("Access token is required."); + throw new AuthenticationException("Access token is required."); } var info = GetTokenInfo(request); if (info == null) { - throw new SecurityException("Access token is invalid or expired."); + throw new AuthenticationException("Access token is invalid or expired."); } //if (!string.IsNullOrEmpty(info.UserId)) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 2292d86a4a..0680c5ffe7 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -1,15 +1,18 @@ -using System; +#nullable enable + +using System; +using System.Buffers; +using System.IO.Pipelines; +using System.Net; using System.Net.WebSockets; -using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.Net; +using MediaBrowser.Common.Json; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using UtfUnknown; namespace Emby.Server.Implementations.HttpServer { @@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer private readonly ILogger _logger; /// <summary> - /// The json serializer. + /// The json serializer options. /// </summary> - private readonly IJsonSerializer _jsonSerializer; + private readonly JsonSerializerOptions _jsonOptions; /// <summary> /// The socket. /// </summary> - private readonly IWebSocket _socket; + private readonly WebSocket _socket; /// <summary> /// Initializes a new instance of the <see cref="WebSocketConnection" /> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="socket">The socket.</param> /// <param name="remoteEndPoint">The remote end point.</param> - /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="logger">The logger.</param> - /// <exception cref="ArgumentNullException">socket</exception> - public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger) + /// <param name="query">The query.</param> + public WebSocketConnection( + ILogger<WebSocketConnection> logger, + WebSocket socket, + IPAddress? remoteEndPoint, + IQueryCollection query) { - if (socket == null) - { - throw new ArgumentNullException(nameof(socket)); - } - - if (string.IsNullOrEmpty(remoteEndPoint)) - { - throw new ArgumentNullException(nameof(remoteEndPoint)); - } - - if (jsonSerializer == null) - { - throw new ArgumentNullException(nameof(jsonSerializer)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - Id = Guid.NewGuid(); - _jsonSerializer = jsonSerializer; + _logger = logger; _socket = socket; - _socket.OnReceiveBytes = OnReceiveInternal; - RemoteEndPoint = remoteEndPoint; - _logger = logger; + QueryString = query; - socket.Closed += OnSocketClosed; + _jsonOptions = JsonDefaults.GetOptions(); + LastActivityDate = DateTime.Now; } /// <inheritdoc /> - public event EventHandler<EventArgs> Closed; + public event EventHandler<EventArgs>? Closed; /// <summary> /// Gets or sets the remote end point. /// </summary> - public string RemoteEndPoint { get; private set; } + public IPAddress? RemoteEndPoint { get; } /// <summary> /// Gets or sets the receive action. /// </summary> /// <value>The receive action.</value> - public Func<WebSocketMessageInfo, Task> OnReceive { get; set; } + public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; } /// <summary> /// Gets the last activity date. @@ -94,23 +78,14 @@ namespace Emby.Server.Implementations.HttpServer /// <value>The last activity date.</value> public DateTime LastActivityDate { get; private set; } - /// <summary> - /// Gets the id. - /// </summary> - /// <value>The id.</value> - public Guid Id { get; private set; } - - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } + /// <inheritdoc /> + public DateTime LastKeepAliveDate { get; set; } /// <summary> /// Gets or sets the query string. /// </summary> /// <value>The query string.</value> - public IQueryCollection QueryString { get; set; } + public IQueryCollection QueryString { get; } /// <summary> /// Gets the state. @@ -118,119 +93,151 @@ namespace Emby.Server.Implementations.HttpServer /// <value>The state.</value> public WebSocketState State => _socket.State; - void OnSocketClosed(object sender, EventArgs e) + /// <summary> + /// Sends a message asynchronously. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="message">The message.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) { - Closed?.Invoke(this, EventArgs.Empty); + var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); + return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); } - /// <summary> - /// Called when [receive]. - /// </summary> - /// <param name="bytes">The bytes.</param> - private void OnReceiveInternal(byte[] bytes) + /// <inheritdoc /> + public async Task ProcessAsync(CancellationToken cancellationToken = default) { - LastActivityDate = DateTime.UtcNow; + var pipe = new Pipe(); + var writer = pipe.Writer; - if (OnReceive == null) + ValueWebSocketReceiveResult receiveresult; + do { - return; - } - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; + // Allocate at least 512 bytes from the PipeWriter + Memory<byte> memory = writer.GetMemory(512); + try + { + receiveresult = await _socket.ReceiveAsync(memory, cancellationToken); + } + catch (WebSocketException ex) + { + _logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message); + break; + } - if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)) - { - OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length)); - } - else + int bytesRead = receiveresult.Count; + if (bytesRead == 0) + { + break; + } + + // Tell the PipeWriter how much was read from the Socket + writer.Advance(bytesRead); + + // Make the data available to the PipeReader + FlushResult flushResult = await writer.FlushAsync(); + if (flushResult.IsCompleted) + { + // The PipeReader stopped reading + break; + } + + LastActivityDate = DateTime.UtcNow; + + if (receiveresult.EndOfMessage) + { + await ProcessInternal(pipe.Reader).ConfigureAwait(false); + } + } while ( + (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting) + && receiveresult.MessageType != WebSocketMessageType.Close); + + Closed?.Invoke(this, EventArgs.Empty); + + if (_socket.State == WebSocketState.Open + || _socket.State == WebSocketState.CloseReceived + || _socket.State == WebSocketState.CloseSent) { - OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length)); + await _socket.CloseAsync( + WebSocketCloseStatus.NormalClosure, + string.Empty, + cancellationToken).ConfigureAwait(false); } } - private void OnReceiveInternal(string message) + private async Task ProcessInternal(PipeReader reader) { - LastActivityDate = DateTime.UtcNow; - - if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase)) - { - // This info is useful sometimes but also clogs up the log - _logger.LogDebug("Received web socket message that is not a json structure: {message}", message); - return; - } + ReadResult result = await reader.ReadAsync().ConfigureAwait(false); + ReadOnlySequence<byte> buffer = result.Buffer; if (OnReceive == null) { + // Tell the PipeReader how much of the buffer we have consumed + reader.AdvanceTo(buffer.End); return; } + WebSocketMessage<object> stub; try { - var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>)); - var info = new WebSocketMessageInfo + if (buffer.IsSingleSegment) { - MessageType = stub.MessageType, - Data = stub.Data?.ToString(), - Connection = this - }; - - OnReceive(info); + stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions); + } + else + { + var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length)); + try + { + buffer.CopyTo(buf); + stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions); + } + finally + { + ArrayPool<byte>.Shared.Return(buf); + } + } } - catch (Exception ex) + catch (JsonException ex) { + // Tell the PipeReader how much of the buffer we have consumed + reader.AdvanceTo(buffer.End); _logger.LogError(ex, "Error processing web socket message"); + return; } - } - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="message">The message.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">message</exception> - public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) - { - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } + // Tell the PipeReader how much of the buffer we have consumed + reader.AdvanceTo(buffer.End); - var json = _jsonSerializer.SerializeToString(message); + _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub); - return SendAsync(json, cancellationToken); - } + var info = new WebSocketMessageInfo + { + MessageType = stub.MessageType, + Data = stub.Data?.ToString(), // Data can be null + Connection = this + }; - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <param name="buffer">The buffer.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(byte[] buffer, CancellationToken cancellationToken) - { - if (buffer == null) + if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal)) { - throw new ArgumentNullException(nameof(buffer)); + await SendKeepAliveResponse(); + } + else + { + await OnReceive(info).ConfigureAwait(false); } - - cancellationToken.ThrowIfCancellationRequested(); - - return _socket.SendAsync(buffer, true, cancellationToken); } - /// <inheritdoc /> - public Task SendAsync(string text, CancellationToken cancellationToken) + private Task SendKeepAliveResponse() { - if (string.IsNullOrEmpty(text)) + LastKeepAliveDate = DateTime.UtcNow; + return SendAsync(new WebSocketMessage<string> { - throw new ArgumentNullException(nameof(text)); - } - - cancellationToken.ThrowIfCancellationRequested(); - - return _socket.SendAsync(text, true, cancellationToken); + MessageType = "KeepAlive" + }, CancellationToken.None); } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index b1fb8cc635..eb5e190aab 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -11,12 +11,18 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; +using Emby.Server.Implementations.Library; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO { public class LibraryMonitor : ILibraryMonitor { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _configurationManager; + private readonly IFileSystem _fileSystem; + /// <summary> /// The file system watchers. /// </summary> @@ -32,38 +38,6 @@ namespace Emby.Server.Implementations.IO /// </summary> private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase); - /// <summary> - /// Any file name ending in any of these will be ignored by the watchers. - /// </summary> - private static readonly HashSet<string> _alwaysIgnoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) - { - "small.jpg", - "albumart.jpg", - - // WMC temp recording directories that will constantly be written to - "TempRec", - "TempSBE" - }; - - private static readonly string[] _alwaysIgnoreSubstrings = new string[] - { - // Synology - "eaDir", - "#recycle", - ".wd_tv", - ".actors" - }; - - private static readonly HashSet<string> _alwaysIgnoreExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) - { - // thumbs.db - ".db", - - // bts sync files - ".bts", - ".sync" - }; - /// <summary> /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. /// </summary> @@ -113,34 +87,23 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - Logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path); + _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path); } } } - /// <summary> - /// Gets or sets the logger. - /// </summary> - /// <value>The logger.</value> - private ILogger Logger { get; set; } - - private ILibraryManager LibraryManager { get; set; } - private IServerConfigurationManager ConfigurationManager { get; set; } - - private readonly IFileSystem _fileSystem; - /// <summary> /// Initializes a new instance of the <see cref="LibraryMonitor" /> class. /// </summary> public LibraryMonitor( - ILoggerFactory loggerFactory, + ILogger<LibraryMonitor> logger, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem) { - LibraryManager = libraryManager; - Logger = loggerFactory.CreateLogger(GetType().Name); - ConfigurationManager = configurationManager; + _libraryManager = libraryManager; + _logger = logger; + _configurationManager = configurationManager; _fileSystem = fileSystem; } @@ -151,7 +114,7 @@ namespace Emby.Server.Implementations.IO return false; } - var options = LibraryManager.GetLibraryOptions(item); + var options = _libraryManager.GetLibraryOptions(item); if (options != null) { @@ -163,12 +126,12 @@ namespace Emby.Server.Implementations.IO public void Start() { - LibraryManager.ItemAdded += OnLibraryManagerItemAdded; - LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved; + _libraryManager.ItemAdded += OnLibraryManagerItemAdded; + _libraryManager.ItemRemoved += OnLibraryManagerItemRemoved; var pathsToWatch = new List<string>(); - var paths = LibraryManager + var paths = _libraryManager .RootFolder .Children .Where(IsLibraryMonitorEnabled) @@ -261,7 +224,7 @@ namespace Emby.Server.Implementations.IO if (!Directory.Exists(path)) { // Seeing a crash in the mono runtime due to an exception being thrown on a different thread - Logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path); + _logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path); return; } @@ -297,7 +260,7 @@ namespace Emby.Server.Implementations.IO if (_fileSystemWatchers.TryAdd(path, newWatcher)) { newWatcher.EnableRaisingEvents = true; - Logger.LogInformation("Watching directory " + path); + _logger.LogInformation("Watching directory " + path); } else { @@ -307,7 +270,7 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - Logger.LogError(ex, "Error watching path: {path}", path); + _logger.LogError(ex, "Error watching path: {path}", path); } }); } @@ -333,7 +296,7 @@ namespace Emby.Server.Implementations.IO { using (watcher) { - Logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path); + _logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path); watcher.Created -= OnWatcherChanged; watcher.Deleted -= OnWatcherChanged; @@ -372,7 +335,7 @@ namespace Emby.Server.Implementations.IO var ex = e.GetException(); var dw = (FileSystemWatcher)sender; - Logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path); + _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path); DisposeWatcher(dw, true); } @@ -390,7 +353,7 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - Logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath); + _logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath); } } @@ -401,12 +364,7 @@ namespace Emby.Server.Implementations.IO throw new ArgumentNullException(nameof(path)); } - var filename = Path.GetFileName(path); - - var monitorPath = !string.IsNullOrEmpty(filename) && - !_alwaysIgnoreFiles.Contains(filename) && - !_alwaysIgnoreExtensions.Contains(Path.GetExtension(path)) && - _alwaysIgnoreSubstrings.All(i => path.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1); + var monitorPath = !IgnorePatterns.ShouldIgnore(path); // Ignore certain files var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList(); @@ -416,13 +374,13 @@ namespace Emby.Server.Implementations.IO { if (_fileSystem.AreEqual(i, path)) { - Logger.LogDebug("Ignoring change to {Path}", path); + _logger.LogDebug("Ignoring change to {Path}", path); return true; } if (_fileSystem.ContainsSubPath(i, path)) { - Logger.LogDebug("Ignoring change to {Path}", path); + _logger.LogDebug("Ignoring change to {Path}", path); return true; } @@ -430,7 +388,7 @@ namespace Emby.Server.Implementations.IO var parent = Path.GetDirectoryName(i); if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path)) { - Logger.LogDebug("Ignoring change to {Path}", path); + _logger.LogDebug("Ignoring change to {Path}", path); return true; } @@ -485,7 +443,7 @@ namespace Emby.Server.Implementations.IO } } - var newRefresher = new FileRefresher(path, ConfigurationManager, LibraryManager, Logger); + var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger); newRefresher.Completed += NewRefresher_Completed; _activeRefreshers.Add(newRefresher); } @@ -502,8 +460,8 @@ namespace Emby.Server.Implementations.IO /// </summary> public void Stop() { - LibraryManager.ItemAdded -= OnLibraryManagerItemAdded; - LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved; + _libraryManager.ItemAdded -= OnLibraryManagerItemAdded; + _libraryManager.ItemRemoved -= OnLibraryManagerItemRemoved; foreach (var watcher in _fileSystemWatchers.Values.ToList()) { diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index 16b68170be..acae702f30 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -1,3 +1,5 @@ +using System; + namespace Emby.Server.Implementations { public interface IStartupOptions @@ -36,5 +38,10 @@ namespace Emby.Server.Implementations /// Gets the value of the --plugin-manifest-url command line option. /// </summary> string PluginManifestUrl { get; } + + /// <summary> + /// Gets the value of the --published-server-url command line option. + /// </summary> + Uri PublishedServerUrl { get; } } } diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index bc1398332d..218e5a0c6d 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -1,7 +1,5 @@ using System; using System.IO; -using System.Linq; -using System.Text.RegularExpressions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -16,32 +14,6 @@ namespace Emby.Server.Implementations.Library { private readonly ILibraryManager _libraryManager; - /// <summary> - /// Any folder named in this list will be ignored - /// </summary> - private static readonly string[] _ignoreFolders = - { - "metadata", - "ps3_update", - "ps3_vprm", - "extrafanart", - "extrathumbs", - ".actors", - ".wd_tv", - - // Synology - "@eaDir", - "eaDir", - "#recycle", - - // Qnap - "@Recycle", - ".@__thumb", - "$RECYCLE.BIN", - "System Volume Information", - ".grab", - }; - /// <summary> /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class. /// </summary> @@ -60,23 +32,15 @@ namespace Emby.Server.Implementations.Library return false; } - var filename = fileInfo.Name; - - // Ignore hidden files on UNIX - if (Environment.OSVersion.Platform != PlatformID.Win32NT - && filename[0] == '.') + if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) { return true; } + var filename = fileInfo.Name; + if (fileInfo.IsDirectory) { - // Ignore any folders in our list - if (_ignoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase)) - { - return true; - } - if (parent != null) { // Ignore trailer folders but allow it at the collection level @@ -109,11 +73,6 @@ namespace Emby.Server.Implementations.Library return true; } } - - // Ignore samples - Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase); - - return m.Success; } return false; diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index ab036eca7a..52c8facc3e 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library { if (resolvedUser == null) { - throw new ArgumentNullException(nameof(resolvedUser)); + throw new AuthenticationException($"Specified user does not exist."); } bool success = false; diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs new file mode 100644 index 0000000000..d12b5855b2 --- /dev/null +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -0,0 +1,74 @@ +using System.Linq; +using DotNet.Globbing; + +namespace Emby.Server.Implementations.Library +{ + /// <summary> + /// Glob patterns for files to ignore + /// </summary> + public static class IgnorePatterns + { + /// <summary> + /// Files matching these glob patterns will be ignored + /// </summary> + public static readonly string[] Patterns = new string[] + { + "**/small.jpg", + "**/albumart.jpg", + "**/*sample*", + + // Directories + "**/metadata/**", + "**/ps3_update/**", + "**/ps3_vprm/**", + "**/extrafanart/**", + "**/extrathumbs/**", + "**/.actors/**", + "**/.wd_tv/**", + "**/lost+found/**", + + // WMC temp recording directories that will constantly be written to + "**/TempRec/**", + "**/TempSBE/**", + + // Synology + "**/eaDir/**", + "**/@eaDir/**", + "**/#recycle/**", + + // Qnap + "**/@Recycle/**", + "**/.@__thumb/**", + "**/$RECYCLE.BIN/**", + "**/System Volume Information/**", + "**/.grab/**", + + // Unix hidden files and directories + "**/.*/**", + + // thumbs.db + "**/thumbs.db", + + // bts sync files + "**/*.bts", + "**/*.sync", + }; + + private static readonly GlobOptions _globOptions = new GlobOptions + { + Evaluation = { + CaseInsensitive = true + } + }; + + private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray(); + + /// <summary> + /// Returns true if the supplied path should be ignored + /// </summary> + public static bool ShouldIgnore(string path) + { + return _globs.Any(g => g.IsMatch(path)); + } + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 4e2cf334c0..0b86b2db7e 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -54,9 +54,29 @@ namespace Emby.Server.Implementations.Library /// </summary> public class LibraryManager : ILibraryManager { + private readonly ILogger _logger; + private readonly ITaskManager _taskManager; + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly IServerConfigurationManager _configurationManager; + private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory; + private readonly Lazy<IProviderManager> _providerManagerFactory; + private readonly Lazy<IUserViewManager> _userviewManagerFactory; + private readonly IServerApplicationHost _appHost; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly IItemRepository _itemRepository; + private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache; + private NamingOptions _namingOptions; private string[] _videoFileExtensions; + private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value; + + private IProviderManager ProviderManager => _providerManagerFactory.Value; + + private IUserViewManager UserViewManager => _userviewManagerFactory.Value; + /// <summary> /// Gets or sets the postscan tasks. /// </summary> @@ -89,12 +109,6 @@ namespace Emby.Server.Implementations.Library /// <value>The comparers.</value> private IBaseItemComparer[] Comparers { get; set; } - /// <summary> - /// Gets or sets the active item repository - /// </summary> - /// <value>The item repository.</value> - public IItemRepository ItemRepository { get; set; } - /// <summary> /// Occurs when [item added]. /// </summary> @@ -110,90 +124,47 @@ namespace Emby.Server.Implementations.Library /// </summary> public event EventHandler<ItemChangeEventArgs> ItemRemoved; - /// <summary> - /// The _logger - /// </summary> - private readonly ILogger _logger; - - /// <summary> - /// The _task manager - /// </summary> - private readonly ITaskManager _taskManager; - - /// <summary> - /// The _user manager - /// </summary> - private readonly IUserManager _userManager; - - /// <summary> - /// The _user data repository - /// </summary> - private readonly IUserDataManager _userDataRepository; - - /// <summary> - /// Gets or sets the configuration manager. - /// </summary> - /// <value>The configuration manager.</value> - private IServerConfigurationManager ConfigurationManager { get; set; } - - private readonly Func<ILibraryMonitor> _libraryMonitorFactory; - private readonly Func<IProviderManager> _providerManagerFactory; - private readonly Func<IUserViewManager> _userviewManager; public bool IsScanRunning { get; private set; } - private IServerApplicationHost _appHost; - private readonly IMediaEncoder _mediaEncoder; - - /// <summary> - /// The _library items cache - /// </summary> - private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache; - - /// <summary> - /// Gets the library items cache. - /// </summary> - /// <value>The library items cache.</value> - private ConcurrentDictionary<Guid, BaseItem> LibraryItemsCache => _libraryItemsCache; - - private readonly IFileSystem _fileSystem; - /// <summary> /// Initializes a new instance of the <see cref="LibraryManager" /> class. /// </summary> /// <param name="appHost">The application host</param> - /// <param name="loggerFactory">The logger factory.</param> + /// <param name="logger">The logger.</param> /// <param name="taskManager">The task manager.</param> /// <param name="userManager">The user manager.</param> /// <param name="configurationManager">The configuration manager.</param> /// <param name="userDataRepository">The user data repository.</param> public LibraryManager( IServerApplicationHost appHost, - ILoggerFactory loggerFactory, + ILogger<LibraryManager> logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, - Func<ILibraryMonitor> libraryMonitorFactory, + Lazy<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, - Func<IProviderManager> providerManagerFactory, - Func<IUserViewManager> userviewManager, - IMediaEncoder mediaEncoder) + Lazy<IProviderManager> providerManagerFactory, + Lazy<IUserViewManager> userviewManagerFactory, + IMediaEncoder mediaEncoder, + IItemRepository itemRepository) { _appHost = appHost; - _logger = loggerFactory.CreateLogger(nameof(LibraryManager)); + _logger = logger; _taskManager = taskManager; _userManager = userManager; - ConfigurationManager = configurationManager; + _configurationManager = configurationManager; _userDataRepository = userDataRepository; _libraryMonitorFactory = libraryMonitorFactory; _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; - _userviewManager = userviewManager; + _userviewManagerFactory = userviewManagerFactory; _mediaEncoder = mediaEncoder; + _itemRepository = itemRepository; _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>(); - ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; + _configurationManager.ConfigurationUpdated += ConfigurationUpdated; RecordConfigurationValues(configurationManager.Configuration); } @@ -272,7 +243,7 @@ namespace Emby.Server.Implementations.Library /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> private void ConfigurationUpdated(object sender, EventArgs e) { - var config = ConfigurationManager.Configuration; + var config = _configurationManager.Configuration; var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted; @@ -306,7 +277,7 @@ namespace Emby.Server.Implementations.Library } } - LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); + _libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); } public void DeleteItem(BaseItem item, DeleteOptions options) @@ -437,10 +408,10 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); - ItemRepository.DeleteItem(item.Id); + _itemRepository.DeleteItem(item.Id); foreach (var child in children) { - ItemRepository.DeleteItem(child.Id); + _itemRepository.DeleteItem(child.Id); } _libraryItemsCache.TryRemove(item.Id, out BaseItem removed); @@ -509,15 +480,15 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(type)); } - if (key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal)) + if (key.StartsWith(_configurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal)) { // Try to normalize paths located underneath program-data in an attempt to make them more portable - key = key.Substring(ConfigurationManager.ApplicationPaths.ProgramDataPath.Length) + key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length) .TrimStart(new[] { '/', '\\' }) .Replace("/", "\\"); } - if (forceCaseInsensitive || !ConfigurationManager.Configuration.EnableCaseSensitiveItemIds) + if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds) { key = key.ToLowerInvariant(); } @@ -550,7 +521,7 @@ namespace Emby.Server.Implementations.Library collectionType = GetContentTypeOverride(fullPath, true); } - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) + var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService) { Parent = parent, Path = fullPath, @@ -720,7 +691,7 @@ namespace Emby.Server.Implementations.Library /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</exception> public AggregateFolder CreateRootFolder() { - var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath; + var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath; Directory.CreateDirectory(rootFolderPath); @@ -734,7 +705,7 @@ namespace Emby.Server.Implementations.Library } // Add in the plug-in folders - var path = Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "playlists"); + var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists"); Directory.CreateDirectory(path); @@ -786,7 +757,7 @@ namespace Emby.Server.Implementations.Library { if (_userRootFolder == null) { - var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; _logger.LogDebug("Creating userRootPath at {path}", userRootPath); Directory.CreateDirectory(userRootPath); @@ -980,7 +951,7 @@ namespace Emby.Server.Implementations.Library where T : BaseItem, new() { var path = getPathFn(name); - var forceCaseInsensitiveId = ConfigurationManager.Configuration.EnableNormalizedItemByNameIds; + var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds; return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId); } @@ -994,7 +965,7 @@ namespace Emby.Server.Implementations.Library public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress) { // Ensure the location is available. - Directory.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath); + Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath); return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress); } @@ -1031,7 +1002,7 @@ namespace Emby.Server.Implementations.Library public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken) { IsScanRunning = true; - _libraryMonitorFactory().Stop(); + LibraryMonitor.Stop(); try { @@ -1039,7 +1010,7 @@ namespace Emby.Server.Implementations.Library } finally { - _libraryMonitorFactory().Start(); + LibraryMonitor.Start(); IsScanRunning = false; } } @@ -1148,7 +1119,7 @@ namespace Emby.Server.Implementations.Library progress.Report(percent * 100); } - ItemRepository.UpdateInheritedValues(cancellationToken); + _itemRepository.UpdateInheritedValues(cancellationToken); progress.Report(100); } @@ -1168,9 +1139,9 @@ namespace Emby.Server.Implementations.Library var topLibraryFolders = GetUserRootFolder().Children.ToList(); _logger.LogDebug("Getting refreshQueue"); - var refreshQueue = includeRefreshState ? _providerManagerFactory().GetRefreshQueue() : null; + var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null; - return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath) + return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath) .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue)) .ToList(); } @@ -1245,7 +1216,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Guid can't be empty", nameof(id)); } - if (LibraryItemsCache.TryGetValue(id, out BaseItem item)) + if (_libraryItemsCache.TryGetValue(id, out BaseItem item)) { return item; } @@ -1276,7 +1247,7 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User, allowExternalContent); } - return ItemRepository.GetItemList(query); + return _itemRepository.GetItemList(query); } public List<BaseItem> GetItemList(InternalItemsQuery query) @@ -1300,7 +1271,7 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User); } - return ItemRepository.GetCount(query); + return _itemRepository.GetCount(query); } public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents) @@ -1315,7 +1286,7 @@ namespace Emby.Server.Implementations.Library } } - return ItemRepository.GetItemList(query); + return _itemRepository.GetItemList(query); } public QueryResult<BaseItem> QueryItems(InternalItemsQuery query) @@ -1327,12 +1298,12 @@ namespace Emby.Server.Implementations.Library if (query.EnableTotalRecordCount) { - return ItemRepository.GetItems(query); + return _itemRepository.GetItems(query); } return new QueryResult<BaseItem> { - Items = ItemRepository.GetItemList(query).ToArray() + Items = _itemRepository.GetItemList(query).ToArray() }; } @@ -1343,7 +1314,7 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User); } - return ItemRepository.GetItemIdsList(query); + return _itemRepository.GetItemIdsList(query); } public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query) @@ -1354,7 +1325,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetStudios(query); + return _itemRepository.GetStudios(query); } public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query) @@ -1365,7 +1336,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetGenres(query); + return _itemRepository.GetGenres(query); } public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query) @@ -1376,7 +1347,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetMusicGenres(query); + return _itemRepository.GetMusicGenres(query); } public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query) @@ -1387,7 +1358,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetAllArtists(query); + return _itemRepository.GetAllArtists(query); } public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query) @@ -1398,7 +1369,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetArtists(query); + return _itemRepository.GetArtists(query); } private void SetTopParentOrAncestorIds(InternalItemsQuery query) @@ -1439,7 +1410,7 @@ namespace Emby.Server.Implementations.Library } SetTopParentOrAncestorIds(query); - return ItemRepository.GetAlbumArtists(query); + return _itemRepository.GetAlbumArtists(query); } public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query) @@ -1460,10 +1431,10 @@ namespace Emby.Server.Implementations.Library if (query.EnableTotalRecordCount) { - return ItemRepository.GetItems(query); + return _itemRepository.GetItems(query); } - var list = ItemRepository.GetItemList(query); + var list = _itemRepository.GetItemList(query); return new QueryResult<BaseItem> { @@ -1509,7 +1480,7 @@ namespace Emby.Server.Implementations.Library string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && query.ItemIds.Length == 0) { - var userViews = _userviewManager().GetUserViews(new UserViewQuery + var userViews = UserViewManager.GetUserViews(new UserViewQuery { UserId = user.Id, IncludeHidden = true, @@ -1809,7 +1780,7 @@ namespace Emby.Server.Implementations.Library // Don't iterate multiple times var itemsList = items.ToList(); - ItemRepository.SaveItems(itemsList, cancellationToken); + _itemRepository.SaveItems(itemsList, cancellationToken); foreach (var item in itemsList) { @@ -1846,7 +1817,7 @@ namespace Emby.Server.Implementations.Library public void UpdateImages(BaseItem item) { - ItemRepository.SaveImages(item); + _itemRepository.SaveImages(item); RegisterItem(item); } @@ -1863,7 +1834,7 @@ namespace Emby.Server.Implementations.Library { if (item.IsFileProtocol) { - _providerManagerFactory().SaveMetadata(item, updateReason); + ProviderManager.SaveMetadata(item, updateReason); } item.DateLastSaved = DateTime.UtcNow; @@ -1871,7 +1842,7 @@ namespace Emby.Server.Implementations.Library RegisterItem(item); } - ItemRepository.SaveItems(itemsList, cancellationToken); + _itemRepository.SaveItems(itemsList, cancellationToken); if (ItemUpdated != null) { @@ -1947,7 +1918,7 @@ namespace Emby.Server.Implementations.Library /// <returns>BaseItem.</returns> public BaseItem RetrieveItem(Guid id) { - return ItemRepository.RetrieveItem(id); + return _itemRepository.RetrieveItem(id); } public List<Folder> GetCollectionFolders(BaseItem item) @@ -2066,7 +2037,7 @@ namespace Emby.Server.Implementations.Library private string GetContentTypeOverride(string path, bool inherit) { - var nameValuePair = ConfigurationManager.Configuration.ContentTypes + var nameValuePair = _configurationManager.Configuration.ContentTypes .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); @@ -2115,7 +2086,7 @@ namespace Emby.Server.Implementations.Library string sortName) { var path = Path.Combine( - ConfigurationManager.ApplicationPaths.InternalMetadataPath, + _configurationManager.ApplicationPaths.InternalMetadataPath, "views", _fileSystem.GetValidFilename(viewType)); @@ -2147,7 +2118,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); + ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } return item; @@ -2165,7 +2136,7 @@ namespace Emby.Server.Implementations.Library var id = GetNewItemId(idValues, typeof(UserView)); - var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); + var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); var item = GetItemById(id) as UserView; @@ -2202,7 +2173,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { - _providerManagerFactory().QueueRefresh( + ProviderManager.QueueRefresh( item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -2269,7 +2240,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { - _providerManagerFactory().QueueRefresh( + ProviderManager.QueueRefresh( item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -2303,7 +2274,7 @@ namespace Emby.Server.Implementations.Library var id = GetNewItemId(idValues, typeof(UserView)); - var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); + var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); var item = GetItemById(id) as UserView; @@ -2346,7 +2317,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { - _providerManagerFactory().QueueRefresh( + ProviderManager.QueueRefresh( item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -2364,7 +2335,7 @@ namespace Emby.Server.Implementations.Library string videoPath, string[] files) { - new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); + new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); } /// <inheritdoc /> @@ -2675,8 +2646,8 @@ namespace Emby.Server.Implementations.Library } } - var metadataPath = ConfigurationManager.Configuration.MetadataPath; - var metadataNetworkPath = ConfigurationManager.Configuration.MetadataNetworkPath; + var metadataPath = _configurationManager.Configuration.MetadataPath; + var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath; if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath)) { @@ -2687,7 +2658,7 @@ namespace Emby.Server.Implementations.Library } } - foreach (var map in ConfigurationManager.Configuration.PathSubstitutions) + foreach (var map in _configurationManager.Configuration.PathSubstitutions) { if (!string.IsNullOrWhiteSpace(map.From)) { @@ -2756,7 +2727,7 @@ namespace Emby.Server.Implementations.Library public List<PersonInfo> GetPeople(InternalPeopleQuery query) { - return ItemRepository.GetPeople(query); + return _itemRepository.GetPeople(query); } public List<PersonInfo> GetPeople(BaseItem item) @@ -2779,7 +2750,7 @@ namespace Emby.Server.Implementations.Library public List<Person> GetPeopleItems(InternalPeopleQuery query) { - return ItemRepository.GetPeopleNames(query).Select(i => + return _itemRepository.GetPeopleNames(query).Select(i => { try { @@ -2796,7 +2767,7 @@ namespace Emby.Server.Implementations.Library public List<string> GetPeopleNames(InternalPeopleQuery query) { - return ItemRepository.GetPeopleNames(query); + return _itemRepository.GetPeopleNames(query); } public void UpdatePeople(BaseItem item, List<PersonInfo> people) @@ -2806,7 +2777,7 @@ namespace Emby.Server.Implementations.Library return; } - ItemRepository.UpdatePeople(item.Id, people); + _itemRepository.UpdatePeople(item.Id, people); } public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex) @@ -2817,7 +2788,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url); - await _providerManagerFactory().SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false); + await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false); item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); @@ -2850,7 +2821,7 @@ namespace Emby.Server.Implementations.Library name = _fileSystem.GetValidFilename(name); - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, name); while (Directory.Exists(virtualFolderPath)) @@ -2869,7 +2840,7 @@ namespace Emby.Server.Implementations.Library } } - _libraryMonitorFactory().Stop(); + LibraryMonitor.Stop(); try { @@ -2904,7 +2875,7 @@ namespace Emby.Server.Implementations.Library { // Need to add a delay here or directory watchers may still pick up the changes await Task.Delay(1000).ConfigureAwait(false); - _libraryMonitorFactory().Start(); + LibraryMonitor.Start(); } } } @@ -2964,7 +2935,7 @@ namespace Emby.Server.Implementations.Library throw new FileNotFoundException("The network path does not exist."); } - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var shortcutFilename = Path.GetFileNameWithoutExtension(path); @@ -3007,7 +2978,7 @@ namespace Emby.Server.Implementations.Library throw new FileNotFoundException("The network path does not exist."); } - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath); @@ -3060,7 +3031,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(name)); } - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var path = Path.Combine(rootFolderPath, name); @@ -3069,7 +3040,7 @@ namespace Emby.Server.Implementations.Library throw new FileNotFoundException("The media folder does not exist"); } - _libraryMonitorFactory().Stop(); + LibraryMonitor.Stop(); try { @@ -3089,7 +3060,7 @@ namespace Emby.Server.Implementations.Library { // Need to add a delay here or directory watchers may still pick up the changes await Task.Delay(1000).ConfigureAwait(false); - _libraryMonitorFactory().Start(); + LibraryMonitor.Start(); } } } @@ -3103,7 +3074,7 @@ namespace Emby.Server.Implementations.Library var removeList = new List<NameValuePair>(); - foreach (var contentType in ConfigurationManager.Configuration.ContentTypes) + foreach (var contentType in _configurationManager.Configuration.ContentTypes) { if (string.IsNullOrWhiteSpace(contentType.Name)) { @@ -3118,11 +3089,11 @@ namespace Emby.Server.Implementations.Library if (removeList.Count > 0) { - ConfigurationManager.Configuration.ContentTypes = ConfigurationManager.Configuration.ContentTypes + _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes .Except(removeList) - .ToArray(); + .ToArray(); - ConfigurationManager.SaveConfiguration(); + _configurationManager.SaveConfiguration(); } } @@ -3133,7 +3104,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(mediaPath)); } - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); if (!Directory.Exists(virtualFolderPath)) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 70d5bd9f4e..01fe98f3af 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -33,13 +33,13 @@ namespace Emby.Server.Implementations.Library private readonly ILibraryManager _libraryManager; private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; - - private IMediaSourceProvider[] _providers; private readonly ILogger _logger; private readonly IUserDataManager _userDataManager; - private readonly Func<IMediaEncoder> _mediaEncoder; - private ILocalizationManager _localizationManager; - private IApplicationPaths _appPaths; + private readonly IMediaEncoder _mediaEncoder; + private readonly ILocalizationManager _localizationManager; + private readonly IApplicationPaths _appPaths; + + private IMediaSourceProvider[] _providers; public MediaSourceManager( IItemRepository itemRepo, @@ -47,16 +47,16 @@ namespace Emby.Server.Implementations.Library ILocalizationManager localizationManager, IUserManager userManager, ILibraryManager libraryManager, - ILoggerFactory loggerFactory, + ILogger<MediaSourceManager> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, - Func<IMediaEncoder> mediaEncoder) + IMediaEncoder mediaEncoder) { _itemRepo = itemRepo; _userManager = userManager; _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(nameof(MediaSourceManager)); + _logger = logger; _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; _userDataManager = userDataManager; @@ -496,7 +496,7 @@ namespace Emby.Server.Implementations.Library // hack - these two values were taken from LiveTVMediaSourceProvider string cacheKey = request.OpenToken; - await new LiveStreamHelper(_mediaEncoder(), _logger, _jsonSerializer, _appPaths) + await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _appPaths) .AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken) .ConfigureAwait(false); } @@ -621,7 +621,7 @@ namespace Emby.Server.Implementations.Library if (liveStreamInfo is IDirectStreamProvider) { - var info = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest + var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest { MediaSource = mediaSource, ExtractChapters = false, @@ -674,7 +674,7 @@ namespace Emby.Server.Implementations.Library mediaSource.AnalyzeDurationMs = 3000; } - mediaInfo = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest + mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest { MediaSource = mediaSource, MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 6b9f4d052c..e27145a1d2 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -35,7 +35,8 @@ namespace Emby.Server.Implementations.Library return null; } - public static int? GetDefaultSubtitleStreamIndex(List<MediaStream> streams, + public static int? GetDefaultSubtitleStreamIndex( + List<MediaStream> streams, string[] preferredLanguages, SubtitlePlaybackMode mode, string audioTrackLanguage) @@ -115,7 +116,8 @@ namespace Emby.Server.Implementations.Library .ThenBy(i => i.Index); } - public static void SetSubtitleStreamScores(List<MediaStream> streams, + public static void SetSubtitleStreamScores( + List<MediaStream> streams, string[] preferredLanguages, SubtitlePlaybackMode mode, string audioTrackLanguage) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 4fdf73b773..06ff3e611b 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Text.RegularExpressions; @@ -12,24 +14,24 @@ namespace Emby.Server.Implementations.Library /// Gets the attribute value. /// </summary> /// <param name="str">The STR.</param> - /// <param name="attrib">The attrib.</param> + /// <param name="attribute">The attrib.</param> /// <returns>System.String.</returns> - /// <exception cref="ArgumentNullException">attrib</exception> - public static string GetAttributeValue(this string str, string attrib) + /// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception> + public static string? GetAttributeValue(this string str, string attribute) { - if (string.IsNullOrEmpty(str)) + if (str.Length == 0) { - throw new ArgumentNullException(nameof(str)); + throw new ArgumentException("String can't be empty.", nameof(str)); } - if (string.IsNullOrEmpty(attrib)) + if (attribute.Length == 0) { - throw new ArgumentNullException(nameof(attrib)); + throw new ArgumentException("String can't be empty.", nameof(attribute)); } - string srch = "[" + attrib + "="; + string srch = "[" + attribute + "="; int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase); - if (start > -1) + if (start != -1) { start += srch.Length; int end = str.IndexOf(']', start); @@ -37,9 +39,9 @@ namespace Emby.Server.Implementations.Library } // for imdbid we also accept pattern matching - if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase)) { - var m = Regex.Match(str, "tt\\d{7}", RegexOptions.IgnoreCase); + var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); return m.Success ? m.Value : null; } diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 34dcbbe285..7ca15b4e55 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -118,10 +118,12 @@ namespace Emby.Server.Implementations.Library { throw new ArgumentNullException(nameof(fileSystem)); } + if (item == null) { throw new ArgumentNullException(nameof(item)); } + if (args == null) { throw new ArgumentNullException(nameof(args)); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 85b1b6e323..6c9ba7c272 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// </summary> public class MusicAlbumResolver : ItemResolver<MusicAlbum> { - private readonly ILogger _logger; + private readonly ILogger<MusicAlbumResolver> _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <param name="logger">The logger.</param> /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> - public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager) + public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager) { _logger = logger; _fileSystem = fileSystem; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 681db4896e..5f5cd0e928 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// </summary> public class MusicArtistResolver : ItemResolver<MusicArtist> { - private readonly ILogger _logger; + private readonly ILogger<MusicAlbumResolver> _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; @@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <summary> /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class. /// </summary> - /// <param name="logger">The logger.</param> + /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param> /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="config">The configuration manager.</param> public MusicArtistResolver( - ILogger<MusicArtistResolver> logger, + ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config) diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 0b93ebeb81..503de0b4e9 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book> { - private readonly string[] _validExtensions = { ".pdf", ".epub", ".mobi", ".cbr", ".cbz", ".azw3" }; + private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" }; protected override Book Resolve(ItemResolveArgs args) { diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 11d6c737ac..59a77607d2 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -17,16 +17,15 @@ namespace Emby.Server.Implementations.Library { public class SearchEngine : ISearchEngine { + private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; - private readonly ILogger _logger; - public SearchEngine(ILoggerFactory loggerFactory, ILibraryManager libraryManager, IUserManager userManager) + public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager) { + _logger = logger; _libraryManager = libraryManager; _userManager = userManager; - - _logger = loggerFactory.CreateLogger("SearchEngine"); } public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 071681b08f..a9772a078d 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -28,25 +28,24 @@ namespace Emby.Server.Implementations.Library private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - - private Func<IUserManager> _userManager; - - public UserDataManager(ILoggerFactory loggerFactory, IServerConfigurationManager config, Func<IUserManager> userManager) + private readonly IUserManager _userManager; + private readonly IUserDataRepository _repository; + + public UserDataManager( + ILogger<UserDataManager> logger, + IServerConfigurationManager config, + IUserManager userManager, + IUserDataRepository repository) { + _logger = logger; _config = config; - _logger = loggerFactory.CreateLogger(GetType().Name); _userManager = userManager; + _repository = repository; } - /// <summary> - /// Gets or sets the repository. - /// </summary> - /// <value>The repository.</value> - public IUserDataRepository Repository { get; set; } - public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken) { - var user = _userManager().GetUserById(userId); + var user = _userManager.GetUserById(userId); SaveUserData(user, item, userData, reason, cancellationToken); } @@ -71,7 +70,7 @@ namespace Emby.Server.Implementations.Library foreach (var key in keys) { - Repository.SaveUserData(userId, key, userData, cancellationToken); + _repository.SaveUserData(userId, key, userData, cancellationToken); } var cacheKey = GetCacheKey(userId, item.Id); @@ -96,9 +95,9 @@ namespace Emby.Server.Implementations.Library /// <returns></returns> public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken) { - var user = _userManager().GetUserById(userId); + var user = _userManager.GetUserById(userId); - Repository.SaveAllUserData(user.InternalId, userData, cancellationToken); + _repository.SaveAllUserData(user.InternalId, userData, cancellationToken); } /// <summary> @@ -108,14 +107,14 @@ namespace Emby.Server.Implementations.Library /// <returns></returns> public List<UserItemData> GetAllUserData(Guid userId) { - var user = _userManager().GetUserById(userId); + var user = _userManager.GetUserById(userId); - return Repository.GetAllUserData(user.InternalId); + return _repository.GetAllUserData(user.InternalId); } public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys) { - var user = _userManager().GetUserById(userId); + var user = _userManager.GetUserById(userId); return GetUserData(user, itemId, keys); } @@ -131,7 +130,7 @@ namespace Emby.Server.Implementations.Library private UserItemData GetUserDataInternal(long internalUserId, List<string> keys) { - var userData = Repository.GetUserData(internalUserId, keys); + var userData = _repository.GetUserData(internalUserId, keys); if (userData != null) { diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 7b17cc913f..d63bc6bda8 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -20,6 +20,7 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; @@ -44,22 +45,14 @@ namespace Emby.Server.Implementations.Library { private readonly object _policySyncLock = new object(); private readonly object _configSyncLock = new object(); - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger _logger; - /// <summary> - /// Gets the active user repository. - /// </summary> - /// <value>The user repository.</value> + private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly IXmlSerializer _xmlSerializer; private readonly IJsonSerializer _jsonSerializer; private readonly INetworkManager _networkManager; - - private readonly Func<IImageProcessor> _imageProcessorFactory; - private readonly Func<IDtoService> _dtoServiceFactory; + private readonly IImageProcessor _imageProcessor; + private readonly Lazy<IDtoService> _dtoServiceFactory; private readonly IServerApplicationHost _appHost; private readonly IFileSystem _fileSystem; private readonly ICryptoProvider _cryptoProvider; @@ -74,13 +67,15 @@ namespace Emby.Server.Implementations.Library private IPasswordResetProvider[] _passwordResetProviders; private DefaultPasswordResetProvider _defaultPasswordResetProvider; + private IDtoService DtoService => _dtoServiceFactory.Value; + public UserManager( ILogger<UserManager> logger, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, - Func<IImageProcessor> imageProcessorFactory, - Func<IDtoService> dtoServiceFactory, + IImageProcessor imageProcessor, + Lazy<IDtoService> dtoServiceFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem, @@ -90,7 +85,7 @@ namespace Emby.Server.Implementations.Library _userRepository = userRepository; _xmlSerializer = xmlSerializer; _networkManager = networkManager; - _imageProcessorFactory = imageProcessorFactory; + _imageProcessor = imageProcessor; _dtoServiceFactory = dtoServiceFactory; _appHost = appHost; _jsonSerializer = jsonSerializer; @@ -264,6 +259,7 @@ namespace Emby.Server.Implementations.Library { if (string.IsNullOrWhiteSpace(username)) { + _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint); throw new ArgumentNullException(nameof(username)); } @@ -319,26 +315,26 @@ namespace Emby.Server.Implementations.Library if (user == null) { + _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint); throw new AuthenticationException("Invalid username or password entered."); } if (user.Policy.IsDisabled) { - throw new AuthenticationException( - string.Format( - CultureInfo.InvariantCulture, - "The {0} account is currently disabled. Please consult with your administrator.", - user.Name)); + _logger.LogInformation("Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint); + throw new SecurityException($"The {user.Name} account is currently disabled. Please consult with your administrator."); } if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) { - throw new AuthenticationException("Forbidden."); + _logger.LogInformation("Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint); + throw new SecurityException("Forbidden."); } if (!user.IsParentalScheduleAllowed()) { - throw new AuthenticationException("User is not allowed access at this time."); + _logger.LogInformation("Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint); + throw new SecurityException("User is not allowed access at this time."); } // Update LastActivityDate and LastLoginDate, then save @@ -351,14 +347,14 @@ namespace Emby.Server.Implementations.Library } ResetInvalidLoginAttemptCount(user); + _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Name); } else { IncrementInvalidLoginAttemptCount(user); + _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", user.Name, remoteEndPoint); } - _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); - return success ? user : null; } @@ -600,7 +596,7 @@ namespace Emby.Server.Implementations.Library try { - _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user); + DtoService.AttachPrimaryImageAspectRatio(dto, user); } catch (Exception ex) { @@ -625,7 +621,7 @@ namespace Emby.Server.Implementations.Library { try { - return _imageProcessorFactory().GetImageCacheTag(item, image); + return _imageProcessor.GetImageCacheTag(item, image); } catch (Exception ex) { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 900f12062f..3efe1ee253 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var stream = new MediaSourceInfo { - EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveRecordings/" + info.Id + "/stream", + EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", EncoderProtocol = MediaProtocol.Http, Path = info.Path, Protocol = MediaProtocol.File, diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 6e903a18ef..a59c1090e5 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -22,9 +22,12 @@ namespace Emby.Server.Implementations.LiveTv { public class LiveTvDtoService { + private const string InternalVersionNumber = "4"; + + private const string ServiceName = "Emby"; + private readonly ILogger _logger; private readonly IImageProcessor _imageProcessor; - private readonly IDtoService _dtoService; private readonly IApplicationHost _appHost; private readonly ILibraryManager _libraryManager; @@ -32,13 +35,13 @@ namespace Emby.Server.Implementations.LiveTv public LiveTvDtoService( IDtoService dtoService, IImageProcessor imageProcessor, - ILoggerFactory loggerFactory, + ILogger<LiveTvDtoService> logger, IApplicationHost appHost, ILibraryManager libraryManager) { _dtoService = dtoService; _imageProcessor = imageProcessor; - _logger = loggerFactory.CreateLogger(nameof(LiveTvDtoService)); + _logger = logger; _appHost = appHost; _libraryManager = libraryManager; } @@ -161,7 +164,6 @@ namespace Emby.Server.Implementations.LiveTv Limit = 1, ImageTypes = new ImageType[] { ImageType.Thumb }, DtoOptions = new DtoOptions(false) - }).FirstOrDefault(); if (librarySeries != null) @@ -179,6 +181,7 @@ namespace Emby.Server.Implementations.LiveTv _logger.LogError(ex, "Error"); } } + image = librarySeries.GetImageInfo(ImageType.Backdrop, 0); if (image != null) { @@ -199,13 +202,12 @@ namespace Emby.Server.Implementations.LiveTv var program = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, ExternalSeriesId = programSeriesId, Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, DtoOptions = new DtoOptions(false), Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null - }).FirstOrDefault(); if (program != null) @@ -232,9 +234,10 @@ namespace Emby.Server.Implementations.LiveTv try { dto.ParentBackdropImageTags = new string[] - { + { _imageProcessor.GetImageCacheTag(program, image) - }; + }; + dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) @@ -255,7 +258,6 @@ namespace Emby.Server.Implementations.LiveTv Limit = 1, ImageTypes = new ImageType[] { ImageType.Thumb }, DtoOptions = new DtoOptions(false) - }).FirstOrDefault(); if (librarySeries != null) @@ -273,6 +275,7 @@ namespace Emby.Server.Implementations.LiveTv _logger.LogError(ex, "Error"); } } + image = librarySeries.GetImageInfo(ImageType.Backdrop, 0); if (image != null) { @@ -298,7 +301,6 @@ namespace Emby.Server.Implementations.LiveTv Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, DtoOptions = new DtoOptions(false) - }).FirstOrDefault(); if (program == null) @@ -311,7 +313,6 @@ namespace Emby.Server.Implementations.LiveTv ImageTypes = new ImageType[] { ImageType.Primary }, DtoOptions = new DtoOptions(false), Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null - }).FirstOrDefault(); } @@ -396,8 +397,6 @@ namespace Emby.Server.Implementations.LiveTv return null; } - private const string InternalVersionNumber = "4"; - public Guid GetInternalChannelId(string serviceName, string externalId) { var name = serviceName + externalId + InternalVersionNumber; @@ -405,7 +404,6 @@ namespace Emby.Server.Implementations.LiveTv return _libraryManager.GetNewItemId(name.ToLowerInvariant(), typeof(LiveTvChannel)); } - private const string ServiceName = "Emby"; public string GetInternalTimerId(string externalId) { var name = ServiceName + externalId + InternalVersionNumber; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index b64fe8634c..1b10f2d27c 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -41,33 +41,32 @@ namespace Emby.Server.Implementations.LiveTv /// </summary> public class LiveTvManager : ILiveTvManager, IDisposable { + private const string ExternalServiceTag = "ExternalServiceId"; + + private const string EtagKey = "ProgramEtag"; + private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly IItemRepository _itemRepo; private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; private readonly IUserDataManager _userDataManager; private readonly ILibraryManager _libraryManager; private readonly ITaskManager _taskManager; - private readonly IJsonSerializer _jsonSerializer; - private readonly Func<IChannelManager> _channelManager; - - private readonly IDtoService _dtoService; private readonly ILocalizationManager _localization; - + private readonly IJsonSerializer _jsonSerializer; + private readonly IFileSystem _fileSystem; + private readonly IChannelManager _channelManager; private readonly LiveTvDtoService _tvDtoService; private ILiveTvService[] _services = Array.Empty<ILiveTvService>(); - private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>(); private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>(); - private readonly IFileSystem _fileSystem; public LiveTvManager( - IServerApplicationHost appHost, IServerConfigurationManager config, - ILoggerFactory loggerFactory, + ILogger<LiveTvManager> logger, IItemRepository itemRepo, - IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, @@ -76,10 +75,11 @@ namespace Emby.Server.Implementations.LiveTv ILocalizationManager localization, IJsonSerializer jsonSerializer, IFileSystem fileSystem, - Func<IChannelManager> channelManager) + IChannelManager channelManager, + LiveTvDtoService liveTvDtoService) { _config = config; - _logger = loggerFactory.CreateLogger(nameof(LiveTvManager)); + _logger = logger; _itemRepo = itemRepo; _userManager = userManager; _libraryManager = libraryManager; @@ -90,8 +90,7 @@ namespace Emby.Server.Implementations.LiveTv _dtoService = dtoService; _userDataManager = userDataManager; _channelManager = channelManager; - - _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory, appHost, _libraryManager); + _tvDtoService = liveTvDtoService; } public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; @@ -178,7 +177,6 @@ namespace Emby.Server.Implementations.LiveTv { Name = i.Name, Id = i.Type - }).ToList(); } @@ -261,6 +259,7 @@ namespace Emby.Server.Implementations.LiveTv var endTime = DateTime.UtcNow; _logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds); } + info.RequiresClosing = true; var idPrefix = service.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_"; @@ -362,30 +361,37 @@ namespace Emby.Server.Implementations.LiveTv { stream.BitRate = null; } + if (stream.Channels.HasValue && stream.Channels <= 0) { stream.Channels = null; } + if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0) { stream.AverageFrameRate = null; } + if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0) { stream.RealFrameRate = null; } + if (stream.Width.HasValue && stream.Width <= 0) { stream.Width = null; } + if (stream.Height.HasValue && stream.Height <= 0) { stream.Height = null; } + if (stream.SampleRate.HasValue && stream.SampleRate <= 0) { stream.SampleRate = null; } + if (stream.Level.HasValue && stream.Level <= 0) { stream.Level = null; @@ -427,7 +433,6 @@ namespace Emby.Server.Implementations.LiveTv } } - private const string ExternalServiceTag = "ExternalServiceId"; private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) { var parentFolderId = parentFolder.Id; @@ -456,6 +461,7 @@ namespace Emby.Server.Implementations.LiveTv { isNew = true; } + item.Tags = channelInfo.Tags; } @@ -463,6 +469,7 @@ namespace Emby.Server.Implementations.LiveTv { isNew = true; } + item.ParentId = parentFolderId; item.ChannelType = channelInfo.ChannelType; @@ -472,24 +479,28 @@ namespace Emby.Server.Implementations.LiveTv { forceUpdate = true; } + item.SetProviderId(ExternalServiceTag, serviceName); if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal)) { forceUpdate = true; } + item.ExternalId = channelInfo.Id; if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal)) { forceUpdate = true; } + item.Number = channelInfo.Number; if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal)) { forceUpdate = true; } + item.Name = channelInfo.Name; if (!item.HasImage(ImageType.Primary)) @@ -518,8 +529,6 @@ namespace Emby.Server.Implementations.LiveTv return item; } - private const string EtagKey = "ProgramEtag"; - private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken) { var id = _tvDtoService.GetInternalProgramId(info.Id); @@ -2482,7 +2491,7 @@ namespace Emby.Server.Implementations.LiveTv .OrderBy(i => i.SortName) .ToList(); - folders.AddRange(_channelManager().GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery + folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery { UserId = user.Id, IsRecordingsFolder = true, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 03ee5bfb65..82b1f3cf1f 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun //OpenedMediaSource.Path = tempFile; //OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; //OpenedMediaSource.SupportsDirectPlay = false; //OpenedMediaSource.SupportsDirectStream = true; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index f5dda79db3..f7c9c736e3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public M3UTunerHost( IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, - ILogger logger, + ILogger<M3UTunerHost> logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, @@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - private static readonly string[] _disallowedSharedStreamExtensions = new string[] + private static readonly string[] _disallowedSharedStreamExtensions = { ".mkv", ".mp4", diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index d63588bbd1..322fbbbaa5 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Net.Http; using System.Threading; @@ -106,7 +107,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts //OpenedMediaSource.Path = tempFile; //OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; //OpenedMediaSource.Path = TempFilePath; @@ -118,6 +119,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts //OpenedMediaSource.SupportsDirectStream = true; //OpenedMediaSource.SupportsTranscoding = true; await taskCompletionSource.Task.ConfigureAwait(false); + if (taskCompletionSource.Task.Exception != null) + { + // Error happened while opening the stream so raise the exception again to inform the caller + throw taskCompletionSource.Task.Exception; + } + + if (!taskCompletionSource.Task.Result) + { + Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath); + throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name)); + } } private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) @@ -139,14 +151,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts cancellationToken).ConfigureAwait(false); } } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { + Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath); + openTaskCompletionSource.TrySetException(ex); } catch (Exception ex) { - Logger.LogError(ex, "Error copying live stream."); + Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath); + openTaskCompletionSource.TrySetException(ex); } + openTaskCompletionSource.TrySetResult(false); + EnableStreamSharing = false; await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false); }); diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index 1363eaf854..20447347b3 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -4,7 +4,7 @@ "Folders": "Fouers", "Favorites": "Gunstelinge", "HeaderFavoriteShows": "Gunsteling Vertonings", - "ValueSpecialEpisodeName": "Spesiaal - {0}", + "ValueSpecialEpisodeName": "Spesiale - {0}", "HeaderAlbumArtists": "Album Kunstenaars", "Books": "Boeke", "HeaderNextUp": "Volgende", diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index f313039a69..d68928fce4 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -9,7 +9,7 @@ "Channels": "القنوات", "ChapterNameValue": "الفصل {0}", "Collections": "مجموعات", - "DeviceOfflineWithName": "قُطِع الاتصال بـ{0}", + "DeviceOfflineWithName": "قُطِع الاتصال ب{0}", "DeviceOnlineWithName": "{0} متصل", "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}", "Favorites": "المفضلة", diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index ef7792356a..4949b10e6a 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -91,5 +91,7 @@ "HeaderNextUp": "এরপরে আসছে", "HeaderLiveTV": "লাইভ টিভি", "HeaderFavoriteSongs": "প্রিয় গানগুলো", - "HeaderFavoriteShows": "প্রিয় শোগুলো" + "HeaderFavoriteShows": "প্রিয় শোগুলো", + "TasksLibraryCategory": "গ্রন্থাগার", + "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ" } diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 2d82993675..7464ac1c09 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -3,19 +3,19 @@ "AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}", "Application": "Aplicació", "Artists": "Artistes", - "AuthenticationSucceededWithUserName": "{0} s'ha autentificat correctament", + "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", "Books": "Llibres", - "CameraImageUploadedFrom": "Una nova imatge de la càmera ha sigut pujada des de {0}", + "CameraImageUploadedFrom": "Una nova imatge de la càmera ha estat pujada des de {0}", "Channels": "Canals", - "ChapterNameValue": "Episodi {0}", + "ChapterNameValue": "Capítol {0}", "Collections": "Col·leccions", "DeviceOfflineWithName": "{0} s'ha desconnectat", "DeviceOnlineWithName": "{0} està connectat", "FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}", "Favorites": "Preferits", - "Folders": "Directoris", + "Folders": "Carpetes", "Genres": "Gèneres", - "HeaderAlbumArtists": "Artistes dels Àlbums", + "HeaderAlbumArtists": "Artistes del Àlbum", "HeaderCameraUploads": "Pujades de Càmera", "HeaderContinueWatching": "Continua Veient", "HeaderFavoriteAlbums": "Àlbums Preferits", diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 992bb9df37..464ca28ca0 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -23,7 +23,7 @@ "HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteSongs": "Oblíbená hudba", - "HeaderLiveTV": "Živá TV", + "HeaderLiveTV": "Televize", "HeaderNextUp": "Nadcházející", "HeaderRecordingGroups": "Skupiny nahrávek", "HomeVideos": "Domáci videa", diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 414430ff7b..82df43be11 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -3,7 +3,7 @@ "AppDeviceValues": "App: {0}, Gerät: {1}", "Application": "Anwendung", "Artists": "Interpreten", - "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifziert", + "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert", "Books": "Bücher", "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen", "Channels": "Kanäle", @@ -99,11 +99,11 @@ "TaskRefreshChannels": "Erneuere Kanäle", "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.", "TaskCleanTranscode": "Lösche Transkodier Pfad", - "TaskUpdatePluginsDescription": "Läd Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.", + "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.", "TaskUpdatePlugins": "Update Plugins", "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.", "TaskRefreshPeople": "Erneuere Schausteller", - "TaskCleanLogsDescription": "Lösche Log Datein die älter als {0} Tage sind.", + "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.", "TaskCleanLogs": "Lösche Log Pfad", "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.", "TaskRefreshLibrary": "Scanne alle Bibliotheken", diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 53e2f58de8..0753ea39d4 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -1,5 +1,5 @@ { - "Albums": "Άλμπουμ", + "Albums": "Άλμπουμς", "AppDeviceValues": "Εφαρμογή: {0}, Συσκευή: {1}", "Application": "Εφαρμογή", "Artists": "Καλλιτέχνες", @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} τελείωσε να παίζει {1} σε {2}", "ValueHasBeenAddedToLibrary": "{0} προστέθηκαν στη βιβλιοθήκη πολυμέσων σας", "ValueSpecialEpisodeName": "Σπέσιαλ - {0}", - "VersionNumber": "Έκδοση {0}" + "VersionNumber": "Έκδοση {0}", + "TaskRefreshPeople": "Ανανέωση Ατόμων", + "TaskCleanLogsDescription": "Διαγράφει τα αρχεία καταγραφής που είναι άνω των {0} ημερών.", + "TaskCleanLogs": "Καθαρισμός Καταλόγου Καταγραφής", + "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και αναζωογονεί τα μεταδεδομένα.", + "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων", + "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο με κεφάλαια.", + "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου", + "TaskCleanCacheDescription": "Τα διαγραμμένα αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον από το σύστημα.", + "TaskCleanCache": "Καθαρισμός Καταλόγου Προσωρινής Μνήμης", + "TasksChannelsCategory": "Κανάλια Διαδικτύου", + "TasksApplicationCategory": "Εφαρμογή", + "TasksLibraryCategory": "Βιβλιοθήκη", + "TasksMaintenanceCategory": "Συντήρηση", + "TaskDownloadMissingSubtitlesDescription": "Αναζητήσεις στο διαδίκτυο όπου λείπουν υπότιτλους με βάση τη διαμόρφωση μεταδεδομένων.", + "TaskDownloadMissingSubtitles": "Λήψη υπότιτλων που λείπουν", + "TaskRefreshChannelsDescription": "Ανανεώνει τις πληροφορίες καναλιού στο διαδικτύου.", + "TaskRefreshChannels": "Ανανέωση Καναλιών", + "TaskCleanTranscodeDescription": "Διαγράφει αρχείου διακωδικοποιητή περισσότερο από μία ημέρα.", + "TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή", + "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.", + "TaskUpdatePlugins": "Ενημέρωση Προσθηκών", + "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας." } diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 1b6c6b5ae4..fc9a10f276 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -24,7 +24,7 @@ "HeaderFavoriteShows": "Programas favoritos", "HeaderFavoriteSongs": "Canciones favoritas", "HeaderLiveTV": "TV en vivo", - "HeaderNextUp": "A Continuación", + "HeaderNextUp": "Siguiente", "HeaderRecordingGroups": "Grupos de grabación", "HomeVideos": "Videos caseros", "Inherit": "Heredar", @@ -44,7 +44,7 @@ "NameInstallFailed": "{0} instalación fallida", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada desconocida", - "NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.", + "NewVersionIsAvailable": "Una nueva versión del servidor Jellyfin está disponible para descargar.", "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible", "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada", "NotificationOptionAudioPlayback": "Se inició la reproducción de audio", @@ -56,7 +56,7 @@ "NotificationOptionPluginInstalled": "Complemento instalado", "NotificationOptionPluginUninstalled": "Complemento desinstalado", "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada", - "NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor", + "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor", "NotificationOptionTaskFailed": "Falla de tarea programada", "NotificationOptionUserLockedOut": "Usuario bloqueado", "NotificationOptionVideoPlayback": "Se inició la reproducción de video", @@ -71,7 +71,7 @@ "ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskStartedWithName": "{0} iniciado", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", - "Shows": "Series", + "Shows": "Programas", "Songs": "Canciones", "StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", @@ -94,25 +94,25 @@ "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versión {0}", "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten basándose en la configuración de los metadatos.", - "TaskDownloadMissingSubtitles": "Descargar subtítulos extraviados", + "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes", "TaskRefreshChannelsDescription": "Actualizar información de canales de internet.", "TaskRefreshChannels": "Actualizar canales", "TaskCleanTranscodeDescription": "Eliminar archivos transcodificados con mas de un día de antigüedad.", - "TaskCleanTranscode": "Limpiar directorio de Transcodificado", + "TaskCleanTranscode": "Limpiar directorio de transcodificación", "TaskUpdatePluginsDescription": "Descargar e instalar actualizaciones para complementos que estén configurados en actualizar automáticamente.", "TaskUpdatePlugins": "Actualizar complementos", - "TaskRefreshPeopleDescription": "Actualizar metadatos de actores y directores en su librería multimedia.", + "TaskRefreshPeopleDescription": "Actualizar metadatos de actores y directores en su biblioteca multimedia.", "TaskRefreshPeople": "Actualizar personas", "TaskCleanLogsDescription": "Eliminar archivos de registro que tengan mas de {0} días de antigüedad.", "TaskCleanLogs": "Limpiar directorio de registros", - "TaskRefreshLibraryDescription": "Escanear su librería multimedia por nuevos archivos y refrescar metadatos.", - "TaskRefreshLibrary": "Escanear librería multimedia", + "TaskRefreshLibraryDescription": "Escanear su biblioteca multimedia por nuevos archivos y refrescar metadatos.", + "TaskRefreshLibrary": "Escanear biblioteca multimedia", "TaskRefreshChapterImagesDescription": "Crear miniaturas de videos que tengan capítulos.", - "TaskRefreshChapterImages": "Extraer imágenes de capitulo", - "TaskCleanCacheDescription": "Eliminar archivos de cache que no se necesiten en el sistema.", - "TaskCleanCache": "Limpiar directorio Cache", - "TasksChannelsCategory": "Canales de Internet", - "TasksApplicationCategory": "Solicitud", + "TaskRefreshChapterImages": "Extraer imágenes de capítulo", + "TaskCleanCacheDescription": "Eliminar archivos de caché que no se necesiten en el sistema.", + "TaskCleanCache": "Limpiar directorio caché", + "TasksChannelsCategory": "Canales de internet", + "TasksApplicationCategory": "Aplicación", "TasksLibraryCategory": "Biblioteca", "TasksMaintenanceCategory": "Mantenimiento" } diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index e0bbe90b36..20b37ec9f2 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -11,21 +11,21 @@ "Collections": "Colecciones", "DeviceOfflineWithName": "{0} se ha desconectado", "DeviceOnlineWithName": "{0} está conectado", - "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}", + "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}", "Favorites": "Favoritos", "Folders": "Carpetas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas del álbum", - "HeaderCameraUploads": "Subidos desde Camara", - "HeaderContinueWatching": "Continuar Viendo", + "HeaderCameraUploads": "Subidas desde la cámara", + "HeaderContinueWatching": "Continuar viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteShows": "Programas favoritos", "HeaderFavoriteSongs": "Canciones favoritas", - "HeaderLiveTV": "TV en Vivo", - "HeaderNextUp": "A Continuación", - "HeaderRecordingGroups": "Grupos de Grabaciones", + "HeaderLiveTV": "TV en vivo", + "HeaderNextUp": "A continuación", + "HeaderRecordingGroups": "Grupos de grabación", "HomeVideos": "Videos caseros", "Inherit": "Heredar", "ItemAddedWithName": "{0} fue agregado a la biblioteca", @@ -41,12 +41,12 @@ "Movies": "Películas", "Music": "Música", "MusicVideos": "Videos musicales", - "NameInstallFailed": "{0} instalación fallida", + "NameInstallFailed": "Falló la instalación de {0}", "NameSeasonNumber": "Temporada {0}", - "NameSeasonUnknown": "Temporada Desconocida", + "NameSeasonUnknown": "Temporada desconocida", "NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.", - "NotificationOptionApplicationUpdateAvailable": "Actualización de aplicación disponible", - "NotificationOptionApplicationUpdateInstalled": "Actualización de aplicación instalada", + "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible", + "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada", "NotificationOptionAudioPlayback": "Reproducción de audio iniciada", "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida", "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida", @@ -56,7 +56,7 @@ "NotificationOptionPluginInstalled": "Complemento instalado", "NotificationOptionPluginUninstalled": "Complemento desinstalado", "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada", - "NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor", + "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor", "NotificationOptionTaskFailed": "Falla de tarea programada", "NotificationOptionUserLockedOut": "Usuario bloqueado", "NotificationOptionVideoPlayback": "Reproducción de video iniciada", @@ -69,48 +69,48 @@ "PluginUpdatedWithName": "{0} fue actualizado", "ProviderValue": "Proveedor: {0}", "ScheduledTaskFailedWithName": "{0} falló", - "ScheduledTaskStartedWithName": "{0} Iniciado", + "ScheduledTaskStartedWithName": "{0} iniciado", "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado", "Shows": "Programas", "Songs": "Canciones", - "StartupEmbyServerIsLoading": "El servidor Jellyfin esta cargando. Por favor intente de nuevo dentro de poco.", + "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.", "SubtitleDownloadFailureForItem": "Falló la descarga de subtítulos para {0}", - "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}", + "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}", "Sync": "Sincronizar", "System": "Sistema", "TvShows": "Programas de TV", "User": "Usuario", - "UserCreatedWithName": "Se ha creado el usuario {0}", - "UserDeletedWithName": "Se ha eliminado el usuario {0}", - "UserDownloadingItemWithValues": "{0} esta descargando {1}", + "UserCreatedWithName": "El usuario {0} ha sido creado", + "UserDeletedWithName": "El usuario {0} ha sido eliminado", + "UserDownloadingItemWithValues": "{0} está descargando {1}", "UserLockedOutWithName": "El usuario {0} ha sido bloqueado", "UserOfflineFromDevice": "{0} se ha desconectado desde {1}", "UserOnlineFromDevice": "{0} está en línea desde {1}", "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}", - "UserPolicyUpdatedWithName": "Las política de usuario ha sido actualizada por {0}", - "UserStartedPlayingItemWithValues": "{0} está reproduciéndose {1} en {2}", - "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducirse {1} en {2}", - "ValueHasBeenAddedToLibrary": "{0} se han añadido a su biblioteca de medios", + "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}", + "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}", + "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}", + "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios", "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versión {0}", - "TaskDownloadMissingSubtitlesDescription": "Buscar subtítulos de internet basado en configuración de metadatos.", - "TaskDownloadMissingSubtitles": "Descargar subtítulos perdidos", - "TaskRefreshChannelsDescription": "Refrescar información de canales de internet.", + "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.", + "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes", + "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.", "TaskRefreshChannels": "Actualizar canales", - "TaskCleanTranscodeDescription": "Eliminar archivos transcodificados que tengan mas de un día.", + "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.", "TaskCleanTranscode": "Limpiar directorio de transcodificado", - "TaskUpdatePluginsDescription": "Descargar y actualizar complementos que están configurados para actualizarse automáticamente.", + "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.", "TaskUpdatePlugins": "Actualizar complementos", - "TaskRefreshPeopleDescription": "Actualizar datos de actores y directores en su librería multimedia.", - "TaskRefreshPeople": "Refrescar persona", - "TaskCleanLogsDescription": "Eliminar archivos de registro con mas de {0} días.", - "TaskCleanLogs": "Directorio de logo limpio", - "TaskRefreshLibraryDescription": "Escanear su librería multimedia para nuevos archivos y refrescar metadatos.", - "TaskRefreshLibrary": "Escanear librería multimerdia", - "TaskRefreshChapterImagesDescription": "Crear miniaturas para videos con capítulos.", - "TaskRefreshChapterImages": "Extraer imágenes de capítulos", - "TaskCleanCacheDescription": "Eliminar archivos cache que ya no se necesiten por el sistema.", - "TaskCleanCache": "Limpiar directorio cache", + "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.", + "TaskRefreshPeople": "Actualizar personas", + "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.", + "TaskCleanLogs": "Limpiar directorio de registros", + "TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios por archivos nuevos y actualiza los metadatos.", + "TaskRefreshLibrary": "Escanear biblioteca de medios", + "TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.", + "TaskRefreshChapterImages": "Extraer imágenes de los capítulos", + "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.", + "TaskCleanCache": "Limpiar directorio caché", "TasksChannelsCategory": "Canales de Internet", "TasksApplicationCategory": "Aplicación", "TasksLibraryCategory": "Biblioteca", diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index de1baada84..e7bd3959bf 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -71,7 +71,7 @@ "ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskStartedWithName": "{0} iniciada", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", - "Shows": "Series", + "Shows": "Mostrar", "Songs": "Canciones", "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.", "SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}", diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index be6f87ee3f..500c292170 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -23,7 +23,7 @@ "HeaderFavoriteEpisodes": "قسمت‌های مورد علاقه", "HeaderFavoriteShows": "سریال‌های مورد علاقه", "HeaderFavoriteSongs": "آهنگ‌های مورد علاقه", - "HeaderLiveTV": "تلویزیون زنده", + "HeaderLiveTV": "پخش زنده", "HeaderNextUp": "قسمت بعدی", "HeaderRecordingGroups": "گروه‌های ضبط", "HomeVideos": "ویدیوهای خانگی", diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index b39adefe70..f8d6e0e09b 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -1,5 +1,5 @@ { - "HeaderLiveTV": "Suorat lähetykset", + "HeaderLiveTV": "Live-TV", "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.", "NameSeasonUnknown": "Tuntematon Kausi", "NameSeasonNumber": "Kausi {0}", @@ -12,7 +12,7 @@ "MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen asetusryhmä {0} on päivitetty", "MessageApplicationUpdatedTo": "Jellyfin palvelin on päivitetty versioon {0}", "MessageApplicationUpdated": "Jellyfin palvelin on päivitetty", - "Latest": "Viimeisin", + "Latest": "Uusimmat", "LabelRunningTimeValue": "Toiston kesto: {0}", "LabelIpAddressValue": "IP-osoite: {0}", "ItemRemovedWithName": "{0} poistettiin kirjastosta", @@ -41,7 +41,7 @@ "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}", "Books": "Kirjat", "AuthenticationSucceededWithUserName": "{0} todennus onnistui", - "Artists": "Esiintyjät", + "Artists": "Artistit", "Application": "Sovellus", "AppDeviceValues": "Sovellus: {0}, Laite: {1}", "Albums": "Albumit", @@ -67,21 +67,21 @@ "UserDownloadingItemWithValues": "{0} lataa {1}", "UserDeletedWithName": "Käyttäjä {0} poistettu", "UserCreatedWithName": "Käyttäjä {0} luotu", - "TvShows": "TV-Ohjelmat", + "TvShows": "TV-sarjat", "Sync": "Synkronoi", - "SubtitleDownloadFailureFromForItem": "Tekstityksen lataaminen epäonnistui {0} - {1}", + "SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry", "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.", "Songs": "Kappaleet", - "Shows": "Ohjelmat", - "ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen", + "Shows": "Sarjat", + "ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen", "ProviderValue": "Tarjoaja: {0}", "Plugin": "Liitännäinen", "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty", - "NotificationOptionVideoPlayback": "Videon toisto aloitettu", - "NotificationOptionUserLockedOut": "Käyttäjä lukittu", + "NotificationOptionVideoPlayback": "Videota toistetaan", + "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos", "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui", - "NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan", - "NotificationOptionPluginUpdateInstalled": "Lisäosan päivitys asennettu", + "NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen", + "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty", "NotificationOptionPluginUninstalled": "Liitännäinen poistettu", "NotificationOptionPluginInstalled": "Liitännäinen asennettu", "NotificationOptionPluginError": "Ongelma liitännäisessä", @@ -90,8 +90,8 @@ "NotificationOptionCameraImageUploaded": "Kameran kuva ladattu", "NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu", "NotificationOptionAudioPlayback": "Toistetaan ääntä", - "NotificationOptionApplicationUpdateInstalled": "Uusi sovellusversio asennettu", - "NotificationOptionApplicationUpdateAvailable": "Sovelluksesta on uusi versio saatavilla", + "NotificationOptionApplicationUpdateInstalled": "Sovelluspäivitys asennettu", + "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla", "TasksMaintenanceCategory": "Ylläpito", "TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä videon metadatatietojen pohjalta.", "TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset", diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 2c9dae6a17..3dcfa68441 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -94,5 +94,24 @@ "ValueSpecialEpisodeName": "Spécial - {0}", "VersionNumber": "Version {0}", "TasksLibraryCategory": "Bibliothèque", - "TasksMaintenanceCategory": "Entretien" + "TasksMaintenanceCategory": "Entretien", + "TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.", + "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants", + "TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.", + "TaskRefreshChannels": "Rafraîchir des chaines", + "TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.", + "TaskCleanTranscode": "Nettoyer le répertoire de transcodage", + "TaskUpdatePluginsDescription": "Télécharger et installer les mises à jours des extensions qui sont configurés pour les m.à.j. automisés.", + "TaskUpdatePlugins": "Mise à jour des extensions", + "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque de médias.", + "TaskRefreshPeople": "Rafraîchir les acteurs", + "TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.", + "TaskCleanLogs": "Nettoyer le répertoire des journaux", + "TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.", + "TaskRefreshChapterImages": "Extraire les images de chapitre", + "TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres", + "TaskRefreshLibrary": "Analyser la bibliothèque de médias", + "TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires", + "TasksApplicationCategory": "Application", + "TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système." } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index d1403c494d..47ebe12540 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -5,17 +5,17 @@ "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} authentifié avec succès", "Books": "Livres", - "CameraImageUploadedFrom": "Une nouvelle photographie a été chargée depuis {0}", + "CameraImageUploadedFrom": "Une photo a été chargée depuis {0}", "Channels": "Chaînes", "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", "DeviceOfflineWithName": "{0} s'est déconnecté", "DeviceOnlineWithName": "{0} est connecté", - "FailedLoginAttemptWithUserName": "Échec de connexion de {0}", + "FailedLoginAttemptWithUserName": "Échec de connexion depuis {0}", "Favorites": "Favoris", "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Artistes d'album", + "HeaderAlbumArtists": "Artistes", "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", @@ -69,7 +69,7 @@ "PluginUpdatedWithName": "{0} a été mis à jour", "ProviderValue": "Fournisseur : {0}", "ScheduledTaskFailedWithName": "{0} a échoué", - "ScheduledTaskStartedWithName": "{0} a commencé", + "ScheduledTaskStartedWithName": "{0} a démarré", "ServerNameNeedsToBeRestarted": "{0} doit être redémarré", "Shows": "Émissions", "Songs": "Chansons", @@ -95,21 +95,21 @@ "VersionNumber": "Version {0}", "TasksChannelsCategory": "Chaines en ligne", "TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.", - "TaskDownloadMissingSubtitles": "Télécharge les sous-titres manquant", + "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquant", "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.", - "TaskRefreshChannels": "Rafraîchit les chaines", + "TaskRefreshChannels": "Rafraîchir les chaines", "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.", - "TaskCleanTranscode": "Nettoie les dossier des transcodages", - "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des plugins configurés pour être mis à jour automatiquement.", - "TaskUpdatePlugins": "Mettre à jour les plugins", - "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et directeurs dans votre bibliothèque.", - "TaskRefreshPeople": "Rafraîchit les acteurs", + "TaskCleanTranscode": "Nettoyer les dossier des transcodages", + "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour être mises à jour automatiquement.", + "TaskUpdatePlugins": "Mettre à jour les extensions", + "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.", + "TaskRefreshPeople": "Rafraîchir les acteurs", "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", - "TaskCleanLogs": "Nettoie le répertoire des journaux", + "TaskCleanLogs": "Nettoyer le répertoire des journaux", "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.", - "TaskRefreshLibrary": "Scanne toute les Bibliothèques", + "TaskRefreshLibrary": "Scanner toute les Bibliothèques", "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.", - "TaskRefreshChapterImages": "Extrait les images de chapitre", + "TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", "TaskCleanCache": "Vider le répertoire cache", "TasksApplicationCategory": "Application", diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index 9611e33f57..8780a884ba 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -1,41 +1,41 @@ { - "Albums": "Albom", - "AppDeviceValues": "App: {0}, Grät: {1}", - "Application": "Aawändig", - "Artists": "Könstler", - "AuthenticationSucceededWithUserName": "{0} het sech aagmäudet", - "Books": "Büecher", - "CameraImageUploadedFrom": "Es nöis Foti esch ufeglade worde vo {0}", - "Channels": "Kanäu", - "ChapterNameValue": "Kapitu {0}", - "Collections": "Sammlige", - "DeviceOfflineWithName": "{0} esch offline gange", - "DeviceOnlineWithName": "{0} esch online cho", - "FailedLoginAttemptWithUserName": "Fäugschlagne Aamäudeversuech vo {0}", - "Favorites": "Favorite", + "Albums": "Alben", + "AppDeviceValues": "App: {0}, Gerät: {1}", + "Application": "Anwendung", + "Artists": "Künstler", + "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet", + "Books": "Bücher", + "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen", + "Channels": "Kanäle", + "ChapterNameValue": "Kapitel {0}", + "Collections": "Sammlungen", + "DeviceOfflineWithName": "{0} wurde getrennt", + "DeviceOnlineWithName": "{0} ist verbunden", + "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}", + "Favorites": "Favoriten", "Folders": "Ordner", "Genres": "Genres", - "HeaderAlbumArtists": "Albom-Könstler", + "HeaderAlbumArtists": "Album-Künstler", "HeaderCameraUploads": "Kamera-Uploads", - "HeaderContinueWatching": "Wiiterluege", - "HeaderFavoriteAlbums": "Lieblingsalbe", - "HeaderFavoriteArtists": "Lieblings-Interprete", - "HeaderFavoriteEpisodes": "Lieblingsepisode", - "HeaderFavoriteShows": "Lieblingsserie", + "HeaderContinueWatching": "weiter schauen", + "HeaderFavoriteAlbums": "Lieblingsalben", + "HeaderFavoriteArtists": "Lieblings-Künstler", + "HeaderFavoriteEpisodes": "Lieblingsepisoden", + "HeaderFavoriteShows": "Lieblingsserien", "HeaderFavoriteSongs": "Lieblingslieder", - "HeaderLiveTV": "Live-Färnseh", - "HeaderNextUp": "Als nächts", - "HeaderRecordingGroups": "Ufnahmegruppe", - "HomeVideos": "Heimfilmli", - "Inherit": "Hinzuefüege", - "ItemAddedWithName": "{0} esch de Bibliothek dezuegfüegt worde", - "ItemRemovedWithName": "{0} esch vo de Bibliothek entfärnt worde", - "LabelIpAddressValue": "IP-Adrässe: {0}", - "LabelRunningTimeValue": "Loufziit: {0}", - "Latest": "Nöischti", - "MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde", - "MessageApplicationUpdatedTo": "Jellyfin Server esch of Version {0} aktualisiert worde", - "MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde", + "HeaderLiveTV": "Live-Fernseh", + "HeaderNextUp": "Als Nächstes", + "HeaderRecordingGroups": "Aufnahme-Gruppen", + "HomeVideos": "Heimvideos", + "Inherit": "Vererben", + "ItemAddedWithName": "{0} wurde der Bibliothek hinzugefügt", + "ItemRemovedWithName": "{0} wurde aus der Bibliothek entfernt", + "LabelIpAddressValue": "IP-Adresse: {0}", + "LabelRunningTimeValue": "Laufzeit: {0}", + "Latest": "Neueste", + "MessageApplicationUpdated": "Jellyfin-Server wurde aktualisiert", + "MessageApplicationUpdatedTo": "Jellyfin-Server wurde auf Version {0} aktualisiert", + "MessageNamedServerConfigurationUpdatedWithValue": "Der Server-Einstellungsbereich {0} wurde aktualisiert", "MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde", "MixedContent": "Gmeschti Inhäut", "Movies": "Film", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Audiowedergab gstartet", "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt", "NotificationOptionCameraImageUploaded": "Foti ueglade", - "NotificationOptionInstallationFailed": "Installationsfäuer", + "NotificationOptionInstallationFailed": "Installationsfehler", "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt", "NotificationOptionPluginError": "Plugin-Fäuer", "NotificationOptionPluginInstalled": "Plugin installiert", @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt", "ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde", "ValueSpecialEpisodeName": "Extra - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TaskCleanLogs": "Lösche Log Pfad", + "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.", + "TaskRefreshLibrary": "Scanne alle Bibliotheken", + "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.", + "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder", + "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.", + "TaskCleanCache": "Leere Cache Pfad", + "TasksChannelsCategory": "Internet Kanäle", + "TasksApplicationCategory": "Applikation", + "TasksLibraryCategory": "Bibliothek", + "TasksMaintenanceCategory": "Verwaltung", + "TaskDownloadMissingSubtitlesDescription": "Durchsucht das Internet nach fehlenden Untertiteln, basierend auf den Metadaten Einstellungen.", + "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter", + "TaskRefreshChannelsDescription": "Aktualisiert Internet Kanal Informationen.", + "TaskRefreshChannels": "Aktualisiere Kanäle", + "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.", + "TaskCleanTranscode": "Räume Transcodier Verzeichnis auf", + "TaskUpdatePluginsDescription": "Lädt Aktualisierungen für Erweiterungen herunter und installiert diese, für welche automatische Aktualisierungen konfiguriert sind.", + "TaskUpdatePlugins": "Aktualisiere Erweiterungen", + "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schausteller und Regisseure in deiner Bibliothek.", + "TaskRefreshPeople": "Aktualisiere Schauspieler", + "TaskCleanLogsDescription": "Löscht Log Dateien die älter als {0} Tage sind." } diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 1ce8b08a0a..682f5325b5 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -1,7 +1,7 @@ { "Albums": "אלבומים", "AppDeviceValues": "יישום: {0}, מכשיר: {1}", - "Application": "אפליקציה", + "Application": "יישום", "Artists": "אומנים", "AuthenticationSucceededWithUserName": "{0} אומת בהצלחה", "Books": "ספרים", @@ -62,7 +62,7 @@ "NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlaybackStopped": "Video playback stopped", "Photos": "תמונות", - "Playlists": "רשימות ניגון", + "Playlists": "רשימות הפעלה", "Plugin": "Plugin", "PluginInstalledWithName": "{0} was installed", "PluginUninstalledWithName": "{0} was uninstalled", @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}", "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueSpecialEpisodeName": "מיוחד- {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TaskRefreshLibrary": "סרוק ספריית מדיה", + "TaskRefreshChapterImages": "חלץ תמונות פרקים", + "TaskCleanCacheDescription": "מחק קבצי מטמון שלא בשימוש המערכת.", + "TaskCleanCache": "נקה תיקיית מטמון", + "TasksApplicationCategory": "יישום", + "TasksLibraryCategory": "ספרייה", + "TasksMaintenanceCategory": "תחזוקה", + "TaskUpdatePlugins": "עדכן תוספים", + "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.", + "TaskRefreshPeople": "רענן אנשים", + "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.", + "TaskCleanLogs": "נקה תיקיית יומן", + "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.", + "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.", + "TasksChannelsCategory": "ערוצי אינטרנט", + "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.", + "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.", + "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.", + "TaskRefreshChannels": "רענן ערוץ", + "TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.", + "TaskCleanTranscode": "נקה תקיית Transcode", + "TaskUpdatePluginsDescription": "הורד והתקן עדכונים עבור תוספים שמוגדרים לעדכון אוטומטי." } diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 6947178d7a..c169a35e79 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -30,7 +30,7 @@ "Inherit": "Naslijedi", "ItemAddedWithName": "{0} je dodano u biblioteku", "ItemRemovedWithName": "{0} je uklonjen iz biblioteke", - "LabelIpAddressValue": "Ip adresa: {0}", + "LabelIpAddressValue": "IP adresa: {0}", "LabelRunningTimeValue": "Vrijeme rada: {0}", "Latest": "Najnovije", "MessageApplicationUpdated": "Jellyfin Server je ažuriran", @@ -92,5 +92,13 @@ "UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}", "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueSpecialEpisodeName": "Specijal - {0}", - "VersionNumber": "Verzija {0}" + "VersionNumber": "Verzija {0}", + "TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.", + "TaskRefreshLibrary": "Skeniraj medijsku knjižnicu", + "TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.", + "TaskRefreshChapterImages": "Raspakiraj slike poglavlja", + "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.", + "TaskCleanCache": "Očisti priručnu memoriju", + "TasksApplicationCategory": "Aplikacija", + "TasksMaintenanceCategory": "Održavanje" } diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index ef2a57e8e8..0f0f9130b0 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -80,16 +80,32 @@ "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt", "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}", "UserStartedPlayingItemWithValues": "{0} er að spila {1} á {2}", - "UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir notanda {0}", + "UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir {0}", "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt", "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}", "UserOfflineFromDevice": "{0} hefur aftengst frá {1}", - "UserLockedOutWithName": "Notanda {0} hefur verið hindraður aðgangur", + "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur", "UserDownloadingItemWithValues": "{0} Hleður niður {1}", "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", "ProviderValue": "Veitandi: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", "ValueSpecialEpisodeName": "Sérstakt - {0}", - "Shows": "Þættir", - "Playlists": "Spilunarlisti" + "Shows": "Sýningar", + "Playlists": "Spilunarlisti", + "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.", + "TaskRefreshChannels": "Endurhlaða Rásir", + "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.", + "TaskCleanTranscode": "Hreinsa Umkóðunarmöppu", + "TaskUpdatePluginsDescription": "Sækja og setja upp uppfærslur fyrir viðbætur sem eru stilltar til að uppfæra sjálfkrafa.", + "TaskUpdatePlugins": "Uppfæra viðbætur", + "TaskRefreshPeopleDescription": "Uppfærir lýsigögn fyrir leikara og leikstjóra í miðlasafninu þínu.", + "TaskRefreshLibraryDescription": "Skannar miðlasafnið þitt fyrir nýjum skrám og uppfærir lýsigögn.", + "TaskRefreshLibrary": "Skanna miðlasafn", + "TaskRefreshChapterImagesDescription": "Býr til smámyndir fyrir myndbönd sem hafa kaflaskil.", + "TaskCleanCacheDescription": "Eyðir skrám í skyndiminni sem ekki er lengur þörf fyrir í kerfinu.", + "TaskCleanCache": "Hreinsa skráasafn skyndiminnis", + "TasksChannelsCategory": "Netrásir", + "TasksApplicationCategory": "Forrit", + "TasksLibraryCategory": "Miðlasafn", + "TasksMaintenanceCategory": "Viðhald" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 0758bbe9ce..7f5a56e86c 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -5,7 +5,7 @@ "Artists": "Artisti", "AuthenticationSucceededWithUserName": "{0} autenticato con successo", "Books": "Libri", - "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera dal device {0}", + "CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}", "Channels": "Canali", "ChapterNameValue": "Capitolo {0}", "Collections": "Collezioni", diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index 5e017d4c4c..a4d9f9ef6b 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -104,13 +104,14 @@ "TasksMaintenanceCategory": "メンテナンス", "TaskRefreshChannelsDescription": "ネットチャンネルの情報をリフレッシュします。", "TaskRefreshChannels": "チャンネルのリフレッシュ", - "TaskCleanTranscodeDescription": "一日以上前のトランスコードを消去します。", - "TaskCleanTranscode": "トランスコード用のディレクトリの掃除", + "TaskCleanTranscodeDescription": "1日以上経過したトランスコードファイルを削除します。", + "TaskCleanTranscode": "トランスコードディレクトリの削除", "TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。", "TaskUpdatePlugins": "プラグインの更新", - "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータをリフレッシュします。", - "TaskRefreshPeople": "俳優や監督のデータのリフレッシュ", + "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータを更新します。", + "TaskRefreshPeople": "俳優や監督のデータの更新", "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。", "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。", - "TaskRefreshChapterImages": "チャプター画像を抽出する" + "TaskRefreshChapterImages": "チャプター画像を抽出する", + "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする" } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 01a740187d..35053766b4 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}", "ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką", "ValueSpecialEpisodeName": "Ypatinga - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.", + "TaskUpdatePlugins": "Atnaujinti Priedus", + "TaskDownloadMissingSubtitlesDescription": "Ieško internete trūkstamų subtitrų remiantis metaduomenų konfigūracija.", + "TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.", + "TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija", + "TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.", + "TaskRefreshLibrary": "Skenuoti Mediateka", + "TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus", + "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informacija.", + "TaskRefreshChannels": "Atnaujinti Kanalus", + "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.", + "TaskRefreshPeople": "Atnaujinti Žmones", + "TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.", + "TaskCleanLogs": "Išvalyti Žurnalą", + "TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.", + "TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus", + "TaskCleanCache": "Išvalyti Talpyklą", + "TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.", + "TasksChannelsCategory": "Internetiniai Kanalai", + "TasksApplicationCategory": "Programa", + "TasksLibraryCategory": "Mediateka", + "TasksMaintenanceCategory": "Priežiūra" } diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json index 8df137302f..bbdf99abab 100644 --- a/Emby.Server.Implementations/Localization/Core/mk.json +++ b/Emby.Server.Implementations/Localization/Core/mk.json @@ -91,5 +91,12 @@ "Songs": "Песни", "Shows": "Серии", "ServerNameNeedsToBeRestarted": "{0} треба да се рестартира", - "ScheduledTaskStartedWithName": "{0} започна" + "ScheduledTaskStartedWithName": "{0} започна", + "TaskRefreshChapterImages": "Извези Слики од Поглавје", + "TaskCleanCacheDescription": "Ги брише кешираните фајлови што не се повеќе потребни од системот.", + "TaskCleanCache": "Исчисти Го Кешот", + "TasksChannelsCategory": "Интернет Канали", + "TasksApplicationCategory": "Апликација", + "TasksLibraryCategory": "Библиотека", + "TasksMaintenanceCategory": "Одржување" } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index e523ae90ba..5637ce3462 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -96,5 +96,10 @@ "TasksChannelsCategory": "Internett kanaler", "TasksApplicationCategory": "Applikasjon", "TasksLibraryCategory": "Bibliotek", - "TasksMaintenanceCategory": "Vedlikehold" + "TasksMaintenanceCategory": "Vedlikehold", + "TaskCleanCache": "Tøm buffer katalog", + "TaskRefreshLibrary": "Skann mediebibliotek", + "TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.", + "TaskRefreshChapterImages": "Trekk ut Kapittelbilder", + "TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet." } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 3bc9c2a779..41c74d54de 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -1,11 +1,11 @@ { "Albums": "Albums", "AppDeviceValues": "App: {0}, Apparaat: {1}", - "Application": "Applicatie", + "Application": "Programma", "Artists": "Artiesten", - "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd", + "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd", "Books": "Boeken", - "CameraImageUploadedFrom": "Er is een nieuwe foto toegevoegd van {0}", + "CameraImageUploadedFrom": "Er is een nieuwe camera afbeelding toegevoegd via {0}", "Channels": "Kanalen", "ChapterNameValue": "Hoofdstuk {0}", "Collections": "Verzamelingen", @@ -26,7 +26,7 @@ "HeaderLiveTV": "Live TV", "HeaderNextUp": "Volgende", "HeaderRecordingGroups": "Opnamegroepen", - "HomeVideos": "Start video's", + "HomeVideos": "Home video's", "Inherit": "Overerven", "ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek", "ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Muziek gestart", "NotificationOptionAudioPlaybackStopped": "Muziek gestopt", "NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload", - "NotificationOptionInstallationFailed": "Installatie mislukking", + "NotificationOptionInstallationFailed": "Installatie mislukt", "NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd", "NotificationOptionPluginError": "Plug-in fout", "NotificationOptionPluginInstalled": "Plug-in geïnstalleerd", diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index e9d9bbf2e1..bdc0d0169b 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} zakończył odtwarzanie {1} na {2}", "ValueHasBeenAddedToLibrary": "{0} został dodany do biblioteki mediów", "ValueSpecialEpisodeName": "Specjalne - {0}", - "VersionNumber": "Wersja {0}" + "VersionNumber": "Wersja {0}", + "TaskDownloadMissingSubtitlesDescription": "Przeszukuje internet w poszukiwaniu brakujących napisów w oparciu o konfigurację metadanych.", + "TaskDownloadMissingSubtitles": "Pobierz brakujące napisy", + "TaskRefreshChannelsDescription": "Odświeża informacje o kanałach internetowych.", + "TaskRefreshChannels": "Odśwież kanały", + "TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.", + "TaskCleanTranscode": "Wyczyść folder transkodowania", + "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów które są skonfigurowane do automatycznej aktualizacji.", + "TaskUpdatePlugins": "Aktualizuj pluginy", + "TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.", + "TaskRefreshPeople": "Odśwież obsadę", + "TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.", + "TaskCleanLogs": "Wyczyść folder logów", + "TaskRefreshLibraryDescription": "Skanuje Twoją bibliotekę mediów dla nowych plików i odświeżenia metadanych.", + "TaskRefreshLibrary": "Skanuj bibliotekę mediów", + "TaskRefreshChapterImagesDescription": "Tworzy miniatury dla filmów posiadających rozdziały.", + "TaskRefreshChapterImages": "Wydobądź grafiki rozdziałów", + "TaskCleanCacheDescription": "Usuwa niepotrzebne i przestarzałe pliki cache.", + "TaskCleanCache": "Wyczyść folder Cache", + "TasksChannelsCategory": "Kanały internetowe", + "TasksApplicationCategory": "Aplikacja", + "TasksLibraryCategory": "Biblioteka", + "TasksMaintenanceCategory": "Konserwacja" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 3a69b6d7a5..275195640b 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -19,10 +19,10 @@ "HeaderCameraUploads": "Envios da Câmera", "HeaderContinueWatching": "Continuar Assistindo", "HeaderFavoriteAlbums": "Álbuns Favoritos", - "HeaderFavoriteArtists": "Artistas Favoritos", - "HeaderFavoriteEpisodes": "Episódios Favoritos", - "HeaderFavoriteShows": "Séries Favoritas", - "HeaderFavoriteSongs": "Músicas Favoritas", + "HeaderFavoriteArtists": "Artistas favoritos", + "HeaderFavoriteEpisodes": "Episódios favoritos", + "HeaderFavoriteShows": "Séries favoritas", + "HeaderFavoriteSongs": "Músicas favoritas", "HeaderLiveTV": "TV ao Vivo", "HeaderNextUp": "A Seguir", "HeaderRecordingGroups": "Grupos de Gravação", diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index ebf35c4920..c1fb65743d 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -26,7 +26,7 @@ "HeaderLiveTV": "TV em Direto", "HeaderNextUp": "A Seguir", "HeaderRecordingGroups": "Grupos de Gravação", - "HomeVideos": "Home videos", + "HomeVideos": "Videos caseiros", "Inherit": "Herdar", "ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca", @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}", "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia", "ValueSpecialEpisodeName": "Especial - {0}", - "VersionNumber": "Versão {0}" + "VersionNumber": "Versão {0}", + "TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas em falta baseado na configuração de metadados.", + "TaskDownloadMissingSubtitles": "Fazer download de legendas em falta", + "TaskRefreshChannelsDescription": "Atualizar informação sobre canais da Internet.", + "TaskRefreshChannels": "Atualizar Canais", + "TaskCleanTranscodeDescription": "Apagar ficheiros de transcode com mais de um dia.", + "TaskCleanTranscode": "Limpar a Diretoria de Transcode", + "TaskUpdatePluginsDescription": "Faz o download e instala updates para os plugins que estão configurados para atualizar automaticamente.", + "TaskUpdatePlugins": "Atualizar Plugins", + "TaskRefreshPeopleDescription": "Atualizar metadados para atores e diretores na biblioteca.", + "TaskRefreshPeople": "Atualizar Pessoas", + "TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.", + "TaskCleanLogs": "Limpar a Diretoria de Logs", + "TaskRefreshLibraryDescription": "Scannear a biblioteca de música para novos ficheiros e atualizar os metadados.", + "TaskRefreshLibrary": "Scannear Biblioteca de Música", + "TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.", + "TaskRefreshChapterImages": "Extrair Imagens dos Capítulos", + "TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.", + "TaskCleanCache": "Limpar Cache", + "TasksChannelsCategory": "Canais da Internet", + "TasksApplicationCategory": "Aplicação", + "TasksLibraryCategory": "Biblioteca", + "TasksMaintenanceCategory": "Manutenção" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 661ee8603e..25c5b9053f 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -95,5 +95,13 @@ "TaskCleanCache": "Limpar Diretório de Cache", "TasksApplicationCategory": "Aplicação", "TasksLibraryCategory": "Biblioteca", - "TasksMaintenanceCategory": "Manutenção" + "TasksMaintenanceCategory": "Manutenção", + "TaskRefreshChannels": "Atualizar Canais", + "TaskUpdatePlugins": "Atualizar Plugins", + "TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.", + "TaskCleanLogs": "Limpar diretório de log", + "TaskRefreshLibrary": "Escanear biblioteca de mídias", + "TaskRefreshChapterImagesDescription": "Criar miniaturas para videos que tem capítulos.", + "TaskCleanCacheDescription": "Deletar arquivos de cache que não são mais usados pelo sistema.", + "TasksChannelsCategory": "Canais de Internet" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index c46aa5c30d..71ee6446ce 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -9,8 +9,8 @@ "Channels": "Каналы", "ChapterNameValue": "Сцена {0}", "Collections": "Коллекции", - "DeviceOfflineWithName": "{0} - подкл. разъ-но", - "DeviceOnlineWithName": "{0} - подкл. уст-но", + "DeviceOfflineWithName": "{0} - отключено", + "DeviceOnlineWithName": "{0} - подключено", "FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна", "Favorites": "Избранное", "Folders": "Папки", @@ -26,30 +26,30 @@ "HeaderLiveTV": "Эфир", "HeaderNextUp": "Очередное", "HeaderRecordingGroups": "Группы записей", - "HomeVideos": "Дом. видео", + "HomeVideos": "Домашнее видео", "Inherit": "Наследуемое", "ItemAddedWithName": "{0} - добавлено в медиатеку", "ItemRemovedWithName": "{0} - изъято из медиатеки", "LabelIpAddressValue": "IP-адрес: {0}", "LabelRunningTimeValue": "Длительность: {0}", - "Latest": "Новейшее", + "Latest": "Последнее", "MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Конфиг-ия сервера (раздел {0}) была обновлена", - "MessageServerConfigurationUpdated": "Конфиг-ия сервера была обновлена", + "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", + "MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена", "MixedContent": "Смешанное содержимое", "Movies": "Кино", "Music": "Музыка", - "MusicVideos": "Муз. видео", + "MusicVideos": "Музыкальные клипы", "NameInstallFailed": "Установка {0} неудачна", "NameSeasonNumber": "Сезон {0}", "NameSeasonUnknown": "Сезон неопознан", "NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.", "NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения", "NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено", - "NotificationOptionAudioPlayback": "Воспр-ие аудио зап-но", - "NotificationOptionAudioPlaybackStopped": "Восп-ие аудио ост-но", - "NotificationOptionCameraImageUploaded": "Произведена выкладка отснятого с камеры", + "NotificationOptionAudioPlayback": "Воспроизведение аудио запущено", + "NotificationOptionAudioPlaybackStopped": "Воспроизведение аудио остановлено", + "NotificationOptionCameraImageUploaded": "Изображения с камеры загружены", "NotificationOptionInstallationFailed": "Сбой установки", "NotificationOptionNewLibraryContent": "Новое содержание добавлено", "NotificationOptionPluginError": "Сбой плагина", @@ -59,8 +59,8 @@ "NotificationOptionServerRestartRequired": "Требуется перезапуск сервера", "NotificationOptionTaskFailed": "Сбой назначенной задачи", "NotificationOptionUserLockedOut": "Пользователь заблокирован", - "NotificationOptionVideoPlayback": "Воспр-ие видео зап-но", - "NotificationOptionVideoPlaybackStopped": "Восп-ие видео ост-но", + "NotificationOptionVideoPlayback": "Воспроизведение видео запущено", + "NotificationOptionVideoPlaybackStopped": "Воспроизведение видео остановлено", "Photos": "Фото", "Playlists": "Плей-листы", "Plugin": "Плагин", @@ -76,21 +76,43 @@ "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}", - "Sync": "Синхро", + "Sync": "Синхронизация", "System": "Система", "TvShows": "ТВ", - "User": "Польз-ль", + "User": "Пользователь", "UserCreatedWithName": "Пользователь {0} был создан", "UserDeletedWithName": "Пользователь {0} был удалён", "UserDownloadingItemWithValues": "{0} загружает {1}", "UserLockedOutWithName": "Пользователь {0} был заблокирован", - "UserOfflineFromDevice": "{0} - подкл. с {1} разъ-но", - "UserOnlineFromDevice": "{0} - подкл. с {1} уст-но", - "UserPasswordChangedWithName": "Пароль польз-ля {0} был изменён", - "UserPolicyUpdatedWithName": "Польз-ие политики {0} были обновлены", - "UserStartedPlayingItemWithValues": "{0} - воспр. «{1}» на {2}", - "UserStoppedPlayingItemWithValues": "{0} - воспр. «{1}» ост-но на {2}", + "UserOfflineFromDevice": "{0} отключился с {1}", + "UserOnlineFromDevice": "{0} подключился с {1}", + "UserPasswordChangedWithName": "Пароль пользователя {0} был изменён", + "UserPolicyUpdatedWithName": "Политики пользователя {0} были обновлены", + "UserStartedPlayingItemWithValues": "{0} - воспроизведение «{1}» на {2}", + "UserStoppedPlayingItemWithValues": "{0} - воспроизведение остановлено «{1}» на {2}", "ValueHasBeenAddedToLibrary": "{0} (добавлено в медиатеку)", - "ValueSpecialEpisodeName": "Спецэпизод - {0}", - "VersionNumber": "Версия {0}" + "ValueSpecialEpisodeName": "Специальный эпизод - {0}", + "VersionNumber": "Версия {0}", + "TaskDownloadMissingSubtitles": "Загрузка отсутствующих субтитров", + "TaskRefreshChannels": "Обновление каналов", + "TaskCleanTranscode": "Очистка каталога перекодировки", + "TaskUpdatePlugins": "Обновление плагинов", + "TaskRefreshPeople": "Обновление метаданных людей", + "TaskCleanLogs": "Очистка каталога журналов", + "TaskRefreshLibrary": "Сканирование медиатеки", + "TaskRefreshChapterImages": "Извлечение изображений сцен", + "TaskCleanCache": "Очистка каталога кеша", + "TasksChannelsCategory": "Интернет-каналы", + "TasksApplicationCategory": "Приложение", + "TasksLibraryCategory": "Медиатека", + "TasksMaintenanceCategory": "Обслуживание", + "TaskDownloadMissingSubtitlesDescription": "Выполняется поиск в Интернете отсутствующих субтитров на основе конфигурации метаданных.", + "TaskRefreshChannelsDescription": "Обновляются данные интернет-каналов.", + "TaskCleanTranscodeDescription": "Удаляются файлы перекодировки старше одного дня.", + "TaskUpdatePluginsDescription": "Загружаются и устанавливаются обновления для плагинов, у которых включено автоматическое обновление.", + "TaskRefreshPeopleDescription": "Обновляются метаданные актеров и режиссёров в медиатеке.", + "TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).", + "TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.", + "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.", + "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе." } diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index b60dd33bd5..60c58d472d 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -92,5 +92,26 @@ "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}", "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici", "ValueSpecialEpisodeName": "Poseben - {0}", - "VersionNumber": "Različica {0}" + "VersionNumber": "Različica {0}", + "TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise", + "TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.", + "TaskRefreshChannels": "Osveži kanale", + "TaskCleanTranscodeDescription": "Izbriše več kot dan stare datoteke prekodiranja.", + "TaskCleanTranscode": "Počisti mapo prekodiranja", + "TaskUpdatePluginsDescription": "Prenese in namesti posodobitve za dodatke, ki imajo omogočene samodejne posodobitve.", + "TaskUpdatePlugins": "Posodobi dodatke", + "TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.", + "TaskRefreshPeople": "Osveži osebe", + "TaskCleanLogsDescription": "Izbriše dnevniške datoteke starejše od {0} dni.", + "TaskCleanLogs": "Počisti mapo dnevnika", + "TaskRefreshLibraryDescription": "Preišče vašo knjižnico za nove datoteke in osveži metapodatke.", + "TaskRefreshLibrary": "Preišči knjižnico predstavnosti", + "TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.", + "TaskRefreshChapterImages": "Izvleči slike poglavij", + "TaskCleanCacheDescription": "Izbriše predpomnjene datoteke, ki niso več potrebne.", + "TaskCleanCache": "Počisti mapo predpomnilnika", + "TasksChannelsCategory": "Spletni kanali", + "TasksApplicationCategory": "Aplikacija", + "TasksLibraryCategory": "Knjižnica", + "TasksMaintenanceCategory": "Vzdrževanje" } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index b7c50394ae..c8662b2cab 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -9,7 +9,7 @@ "Channels": "Kanaler", "ChapterNameValue": "Kapitel {0}", "Collections": "Samlingar", - "DeviceOfflineWithName": "{0} har tappat anslutningen", + "DeviceOfflineWithName": "{0} har kopplat från", "DeviceOnlineWithName": "{0} är ansluten", "FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}", "Favorites": "Favoriter", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats", "NotificationOptionAudioPlaybackStopped": "Ljuduppspelning stoppades", "NotificationOptionCameraImageUploaded": "Kamerabild har laddats upp", - "NotificationOptionInstallationFailed": "Fel vid installation", + "NotificationOptionInstallationFailed": "Installationen misslyckades", "NotificationOptionNewLibraryContent": "Nytt innehåll har lagts till", "NotificationOptionPluginError": "Fel uppstod med tillägget", "NotificationOptionPluginInstalled": "Tillägg har installerats", @@ -113,5 +113,6 @@ "TasksChannelsCategory": "Internetkanaler", "TasksApplicationCategory": "Applikation", "TasksLibraryCategory": "Bibliotek", - "TasksMaintenanceCategory": "Underhåll" + "TasksMaintenanceCategory": "Underhåll", + "TaskRefreshPeople": "Uppdatera Personer" } diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json new file mode 100644 index 0000000000..32538ac035 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -0,0 +1,71 @@ +{ + "ProviderValue": "ผู้ให้บริการ: {0}", + "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว", + "PluginUninstalledWithName": "ถอนการติดตั้ง {0}", + "PluginInstalledWithName": "{0} ได้รับการติดตั้ง", + "Plugin": "Plugin", + "Playlists": "รายการ", + "Photos": "รูปภาพ", + "NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video", + "NotificationOptionVideoPlayback": "เริ่มแสดง Video", + "NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out", + "NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว", + "NotificationOptionServerRestartRequired": "ควร Restart Server", + "NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว", + "NotificationOptionPluginUninstalled": "ถอด Plugin", + "NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว", + "NotificationOptionPluginError": "Plugin ล้มเหลว", + "NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว", + "NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว", + "NotificationOptionCameraImageUploaded": "รูปภาพถูก upload", + "NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง", + "NotificationOptionAudioPlayback": "เริ่มเล่นเสียง", + "NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว", + "NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว", + "NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่", + "NameSeasonUnknown": "ไม่ทราบปี", + "NameSeasonNumber": "ปี {0}", + "NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ", + "MusicVideos": "MV", + "Music": "เพลง", + "Movies": "ภาพยนต์", + "MixedContent": "รายการแบบผสม", + "MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว", + "MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว", + "MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}", + "MessageApplicationUpdated": "Jellyfin Server update แล้ว", + "Latest": "ล่าสุด", + "LabelRunningTimeValue": "เวลาที่เล่น : {0}", + "LabelIpAddressValue": "IP address: {0}", + "ItemRemovedWithName": "{0} ถูกลบจากรายการ", + "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ", + "Inherit": "การสืบทอด", + "HomeVideos": "วีดีโอส่วนตัว", + "HeaderRecordingGroups": "ค่ายบันทึก", + "HeaderNextUp": "ถัดไป", + "HeaderLiveTV": "รายการสด", + "HeaderFavoriteSongs": "เพลงโปรด", + "HeaderFavoriteShows": "รายการโชว์โปรด", + "HeaderFavoriteEpisodes": "ฉากโปรด", + "HeaderFavoriteArtists": "นักแสดงโปรด", + "HeaderFavoriteAlbums": "อัมบั้มโปรด", + "HeaderContinueWatching": "ชมต่อจากเดิม", + "HeaderCameraUploads": "Upload รูปภาพ", + "HeaderAlbumArtists": "อัลบั้มนักแสดง", + "Genres": "ประเภท", + "Folders": "โฟลเดอร์", + "Favorites": "รายการโปรด", + "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}", + "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ", + "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ", + "Collections": "ชุด", + "ChapterNameValue": "บทที่ {0}", + "Channels": "ชาแนล", + "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}", + "Books": "หนังสือ", + "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ", + "Artists": "นักแสดง", + "Application": "แอปพลิเคชั่น", + "AppDeviceValues": "App: {0}, อุปกรณ์: {1}", + "Albums": "อัลบั้ม" +} diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 62d205516d..3cf3482eba 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Ses çalma başladı", "NotificationOptionAudioPlaybackStopped": "Ses çalma durduruldu", "NotificationOptionCameraImageUploaded": "Kamera fotoğrafı yüklendi", - "NotificationOptionInstallationFailed": "Yükleme başarısız oldu", + "NotificationOptionInstallationFailed": "Kurulum hatası", "NotificationOptionNewLibraryContent": "Yeni içerik eklendi", "NotificationOptionPluginError": "Eklenti hatası", "NotificationOptionPluginInstalled": "Eklenti yüklendi", @@ -95,7 +95,24 @@ "VersionNumber": "Versiyon {0}", "TaskCleanCache": "Geçici dosya klasörünü temizle", "TasksChannelsCategory": "İnternet kanalları", - "TasksApplicationCategory": "Yazılım", + "TasksApplicationCategory": "Uygulama", "TasksLibraryCategory": "Kütüphane", - "TasksMaintenanceCategory": "Onarım" + "TasksMaintenanceCategory": "Onarım", + "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.", + "TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.", + "TaskDownloadMissingSubtitles": "Eksik altyazıları indir", + "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.", + "TaskRefreshChannels": "Kanalları Yenile", + "TaskCleanTranscodeDescription": "Bir günü dolmuş dönüştürme bilgisi içeren dosyaları siler.", + "TaskCleanTranscode": "Dönüşüm Dizinini Temizle", + "TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.", + "TaskUpdatePlugins": "Eklentileri Güncelle", + "TaskRefreshPeople": "Kullanıcıları Yenile", + "TaskCleanLogsDescription": "{0} günden eski log dosyalarını siler.", + "TaskCleanLogs": "Log Dizinini Temizle", + "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve bilgileri yeniler.", + "TaskRefreshLibrary": "Medya Kütüphanesini Tara", + "TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.", + "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar", + "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler." } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json new file mode 100644 index 0000000000..b2e0b66fe1 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -0,0 +1,36 @@ +{ + "MusicVideos": "Музичні відео", + "Music": "Музика", + "Movies": "Фільми", + "MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}", + "MessageApplicationUpdated": "Jellyfin Server був оновлений", + "Latest": "Останні", + "LabelIpAddressValue": "IP-адреси: {0}", + "ItemRemovedWithName": "{0} видалено з бібліотеки", + "ItemAddedWithName": "{0} додано до бібліотеки", + "HeaderNextUp": "Наступний", + "HeaderLiveTV": "Ефірне ТБ", + "HeaderFavoriteSongs": "Улюблені пісні", + "HeaderFavoriteShows": "Улюблені шоу", + "HeaderFavoriteEpisodes": "Улюблені серії", + "HeaderFavoriteArtists": "Улюблені виконавці", + "HeaderFavoriteAlbums": "Улюблені альбоми", + "HeaderContinueWatching": "Продовжити перегляд", + "HeaderCameraUploads": "Завантажено з камери", + "HeaderAlbumArtists": "Виконавці альбомів", + "Genres": "Жанри", + "Folders": "Директорії", + "Favorites": "Улюблені", + "DeviceOnlineWithName": "{0} під'єднано", + "DeviceOfflineWithName": "{0} від'єднано", + "Collections": "Колекції", + "ChapterNameValue": "Глава {0}", + "Channels": "Канали", + "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}", + "Books": "Книги", + "AuthenticationSucceededWithUserName": "{0} успішно авторизовані", + "Artists": "Виконавці", + "Application": "Додаток", + "AppDeviceValues": "Додаток: {0}, Пристрій: {1}", + "Albums": "Альбоми" +} diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 224748e611..0804fc9279 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -1,6 +1,6 @@ { "Albums": "專輯", - "AppDeviceValues": "軟體: {0}, 設備: {1}", + "AppDeviceValues": "軟件: {0}, 設備: {1}", "Application": "應用程式", "Artists": "藝人", "AuthenticationSucceededWithUserName": "{0} 授權成功", @@ -11,15 +11,15 @@ "Collections": "合輯", "DeviceOfflineWithName": "{0} 已經斷開連結", "DeviceOnlineWithName": "{0} 已經連接", - "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試", + "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗", "Favorites": "我的最愛", "Folders": "檔案夾", "Genres": "風格", - "HeaderAlbumArtists": "專輯藝術家", + "HeaderAlbumArtists": "專輯藝人", "HeaderCameraUploads": "相機上載", "HeaderContinueWatching": "繼續觀看", "HeaderFavoriteAlbums": "最愛專輯", - "HeaderFavoriteArtists": "最愛藝術家", + "HeaderFavoriteArtists": "最愛的藝人", "HeaderFavoriteEpisodes": "最愛的劇集", "HeaderFavoriteShows": "最愛的節目", "HeaderFavoriteSongs": "最愛的歌曲", @@ -33,14 +33,14 @@ "LabelIpAddressValue": "IP 地址: {0}", "LabelRunningTimeValue": "運行時間: {0}", "Latest": "最新", - "MessageApplicationUpdated": "Jellyfin Server 已更新", + "MessageApplicationUpdated": "Jellyfin 伺服器已更新", "MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已更新", + "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已更新", "MessageServerConfigurationUpdated": "伺服器設定已經更新", - "MixedContent": "Mixed content", + "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", - "MusicVideos": "音樂MV", + "MusicVideos": "音樂視頻", "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知季數", @@ -49,7 +49,7 @@ "NotificationOptionApplicationUpdateInstalled": "應用程式已更新", "NotificationOptionAudioPlayback": "開始播放音頻", "NotificationOptionAudioPlaybackStopped": "已停止播放音頻", - "NotificationOptionCameraImageUploaded": "相機相片已上傳", + "NotificationOptionCameraImageUploaded": "相片已上傳", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "已添加新内容", "NotificationOptionPluginError": "擴充元件錯誤", @@ -63,11 +63,11 @@ "NotificationOptionVideoPlaybackStopped": "已停止播放視頻", "Photos": "相片", "Playlists": "播放清單", - "Plugin": "Plugin", + "Plugin": "插件", "PluginInstalledWithName": "已安裝 {0}", "PluginUninstalledWithName": "已移除 {0}", "PluginUpdatedWithName": "已更新 {0}", - "ProviderValue": "Provider: {0}", + "ProviderValue": "提供者: {0}", "ScheduledTaskFailedWithName": "{0} 任務失敗", "ScheduledTaskStartedWithName": "{0} 任務開始", "ServerNameNeedsToBeRestarted": "{0} 需要重啓", @@ -77,20 +77,41 @@ "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", "Sync": "同步", - "System": "System", + "System": "系統", "TvShows": "電視節目", - "User": "User", - "UserCreatedWithName": "用家 {0} 已創建", - "UserDeletedWithName": "用家 {0} 已移除", + "User": "使用者", + "UserCreatedWithName": "使用者 {0} 已創建", + "UserDeletedWithName": "使用者 {0} 已移除", "UserDownloadingItemWithValues": "{0} 正在下載 {1}", - "UserLockedOutWithName": "用家 {0} 已被鎖定", + "UserLockedOutWithName": "使用者 {0} 已被鎖定", "UserOfflineFromDevice": "{0} 已從 {1} 斷開", "UserOnlineFromDevice": "{0} 已連綫,來自 {1}", - "UserPasswordChangedWithName": "用家 {0} 的密碼已變更", - "UserPolicyUpdatedWithName": "用戶協議已被更新為 {0}", + "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更", + "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}", "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}", "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫", "ValueSpecialEpisodeName": "特典 - {0}", - "VersionNumber": "版本{0}" + "VersionNumber": "版本{0}", + "TaskDownloadMissingSubtitles": "下載遺失的字幕", + "TaskUpdatePlugins": "更新插件", + "TasksApplicationCategory": "應用程式", + "TaskRefreshLibraryDescription": "掃描媒體庫以查找新文件並刷新metadata。", + "TasksMaintenanceCategory": "維護", + "TaskDownloadMissingSubtitlesDescription": "根據metadata配置在互聯網上搜索缺少的字幕。", + "TaskRefreshChannelsDescription": "刷新互聯網頻道信息。", + "TaskRefreshChannels": "刷新頻道", + "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。", + "TaskCleanTranscode": "清理轉碼目錄", + "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。", + "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的metadata。", + "TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。", + "TaskCleanLogs": "清理日誌目錄", + "TaskRefreshLibrary": "掃描媒體庫", + "TaskRefreshChapterImagesDescription": "為帶有章節的視頻創建縮略圖。", + "TaskRefreshChapterImages": "提取章節圖像", + "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。", + "TaskCleanCache": "清理緩存目錄", + "TasksChannelsCategory": "互聯網頻道", + "TasksLibraryCategory": "庫" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index 21034b76f3..a22f66df90 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -20,7 +20,7 @@ "HeaderContinueWatching": "繼續觀賞", "HeaderFavoriteAlbums": "最愛專輯", "HeaderFavoriteArtists": "最愛演出者", - "HeaderFavoriteEpisodes": "最愛級數", + "HeaderFavoriteEpisodes": "最愛影集", "HeaderFavoriteShows": "最愛節目", "HeaderFavoriteSongs": "最愛歌曲", "HeaderLiveTV": "電視直播", @@ -50,10 +50,10 @@ "NotificationOptionCameraImageUploaded": "相機相片已上傳", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "已新增新內容", - "NotificationOptionPluginError": "擴充元件錯誤", - "NotificationOptionPluginInstalled": "擴充元件已安裝", - "NotificationOptionPluginUninstalled": "擴充元件已移除", - "NotificationOptionPluginUpdateInstalled": "已更新擴充元件", + "NotificationOptionPluginError": "插件安裝錯誤", + "NotificationOptionPluginInstalled": "插件已安裝", + "NotificationOptionPluginUninstalled": "插件已移除", + "NotificationOptionPluginUpdateInstalled": "插件已更新", "NotificationOptionServerRestartRequired": "伺服器需要重新啟動", "NotificationOptionTaskFailed": "排程任務失敗", "NotificationOptionUserLockedOut": "使用者已鎖定", @@ -61,7 +61,7 @@ "NotificationOptionVideoPlaybackStopped": "影片停止播放", "Photos": "相片", "Playlists": "播放清單", - "Plugin": "外掛", + "Plugin": "插件", "PluginInstalledWithName": "{0} 已安裝", "PluginUninstalledWithName": "{0} 已移除", "PluginUpdatedWithName": "{0} 已更新", @@ -91,5 +91,27 @@ "VersionNumber": "版本 {0}", "HeaderRecordingGroups": "錄製組", "Inherit": "繼承", - "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕" + "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕", + "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。", + "TaskDownloadMissingSubtitles": "下載遺失的字幕", + "TaskRefreshChannels": "重新整理頻道", + "TaskUpdatePlugins": "更新插件", + "TaskRefreshPeople": "重新整理人員", + "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔案。", + "TaskCleanLogs": "清空紀錄資料夾", + "TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。", + "TaskRefreshLibrary": "掃描媒體庫", + "TaskRefreshChapterImages": "擷取章節圖片", + "TaskCleanCacheDescription": "刪除系統長時間不需要的快取。", + "TaskCleanCache": "清除快取資料夾", + "TasksLibraryCategory": "媒體庫", + "TaskRefreshChannelsDescription": "重新整理網絡頻道資料。", + "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。", + "TaskCleanTranscode": "清除轉碼資料夾", + "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。", + "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。", + "TaskRefreshChapterImagesDescription": "為有章節的視頻創建縮圖。", + "TasksChannelsCategory": "網絡頻道", + "TasksApplicationCategory": "應用程式", + "TasksMaintenanceCategory": "維修" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index bda43e832a..e2a634e1a4 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -23,9 +23,6 @@ namespace Emby.Server.Implementations.Localization private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; - /// <summary> - /// The _configuration manager. - /// </summary> private readonly IServerConfigurationManager _configurationManager; private readonly IJsonSerializer _jsonSerializer; private readonly ILogger _logger; diff --git a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs deleted file mode 100644 index fda32da5e8..0000000000 --- a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager; - -namespace Emby.Server.Implementations.Middleware -{ - public class WebSocketMiddleware - { - private readonly RequestDelegate _next; - private readonly ILogger<WebSocketMiddleware> _logger; - private readonly WebSocketManager _webSocketManager; - - public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager) - { - _next = next; - _logger = logger; - _webSocketManager = webSocketManager; - } - - public async Task Invoke(HttpContext httpContext) - { - _logger.LogInformation("Handling request: " + httpContext.Request.Path); - - if (httpContext.WebSockets.IsWebSocketRequest) - { - var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); - if (webSocketContext != null) - { - await _webSocketManager.OnWebSocketConnected(webSocketContext).ConfigureAwait(false); - } - } - else - { - await _next.Invoke(httpContext).ConfigureAwait(false); - } - } - } -} diff --git a/Emby.Server.Implementations/Net/IWebSocket.cs b/Emby.Server.Implementations/Net/IWebSocket.cs deleted file mode 100644 index 4d160aa66f..0000000000 --- a/Emby.Server.Implementations/Net/IWebSocket.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; - -namespace Emby.Server.Implementations.Net -{ - /// <summary> - /// Interface IWebSocket - /// </summary> - public interface IWebSocket : IDisposable - { - /// <summary> - /// Occurs when [closed]. - /// </summary> - event EventHandler<EventArgs> Closed; - - /// <summary> - /// Gets or sets the state. - /// </summary> - /// <value>The state.</value> - WebSocketState State { get; } - - /// <summary> - /// Gets or sets the receive action. - /// </summary> - /// <value>The receive action.</value> - Action<byte[]> OnReceiveBytes { get; set; } - - /// <summary> - /// Sends the async. - /// </summary> - /// <param name="bytes">The bytes.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken); - - /// <summary> - /// Sends the asynchronous. - /// </summary> - /// <param name="text">The text.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken); - } -} diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs deleted file mode 100644 index 6880766f9a..0000000000 --- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; - -namespace Emby.Server.Implementations.Net -{ - public class WebSocketConnectEventArgs : EventArgs - { - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } - /// <summary> - /// Gets or sets the query string. - /// </summary> - /// <value>The query string.</value> - public IQueryCollection QueryString { get; set; } - /// <summary> - /// Gets or sets the web socket. - /// </summary> - /// <value>The web socket.</value> - public IWebSocket WebSocket { get; set; } - /// <summary> - /// Gets or sets the endpoint. - /// </summary> - /// <value>The endpoint.</value> - public string Endpoint { get; set; } - } -} diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index ecf58dbc0e..6ffa581a9c 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -32,22 +32,8 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue = new ConcurrentQueue<Tuple<Type, TaskOptions>>(); - /// <summary> - /// Gets or sets the json serializer. - /// </summary> - /// <value>The json serializer.</value> private readonly IJsonSerializer _jsonSerializer; - - /// <summary> - /// Gets or sets the application paths. - /// </summary> - /// <value>The application paths.</value> private readonly IApplicationPaths _applicationPaths; - - /// <summary> - /// Gets the logger. - /// </summary> - /// <value>The logger.</value> private readonly ILogger _logger; private readonly IFileSystem _fileSystem; @@ -56,17 +42,17 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <param name="applicationPaths">The application paths.</param> /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="loggerFactory">The logger factory.</param> + /// <param name="logger">The logger.</param> /// <param name="fileSystem">The filesystem manager.</param> public TaskManager( IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, - ILoggerFactory loggerFactory, + ILogger<TaskManager> logger, IFileSystem fileSystem) { _applicationPaths = applicationPaths; _jsonSerializer = jsonSerializer; - _logger = loggerFactory.CreateLogger(nameof(TaskManager)); + _logger = logger; _fileSystem = fileSystem; ScheduledTasks = Array.Empty<IScheduledTaskWorker>(); diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 1ef5c4b996..4e4029f06f 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -15,8 +15,8 @@ namespace Emby.Server.Implementations.Security { public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository { - public AuthenticationRepository(ILoggerFactory loggerFactory, IServerConfigurationManager config) - : base(loggerFactory.CreateLogger(nameof(AuthenticationRepository))) + public AuthenticationRepository(ILogger<AuthenticationRepository> logger, IServerConfigurationManager config) + : base(logger) { DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db"); } diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 2f57c97a13..dfdd4200e0 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -9,8 +9,6 @@ namespace Emby.Server.Implementations /// </summary> public class ServerApplicationPaths : BaseApplicationPaths, IServerApplicationPaths { - private string _internalMetadataPath; - /// <summary> /// Initializes a new instance of the <see cref="ServerApplicationPaths" /> class. /// </summary> @@ -27,6 +25,7 @@ namespace Emby.Server.Implementations cacheDirectoryPath, webDirectoryPath) { + InternalMetadataPath = DefaultInternalMetadataPath; } /// <summary> @@ -98,12 +97,11 @@ namespace Emby.Server.Implementations /// <value>The user configuration directory path.</value> public string UserConfigurationDirectoryPath => Path.Combine(ConfigurationDirectoryPath, "users"); + /// <inheritdoc/> + public string DefaultInternalMetadataPath => Path.Combine(ProgramDataPath, "metadata"); + /// <inheritdoc /> - public string InternalMetadataPath - { - get => _internalMetadataPath ?? (_internalMetadataPath = Path.Combine(DataPath, "metadata")); - set => _internalMetadataPath = value; - } + public string InternalMetadataPath { get; set; } /// <inheritdoc /> public string VirtualInternalMetadataPath { get; } = "%MetadataPath%"; diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs index 23e22afd58..56e23d5492 100644 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using MediaBrowser.Common.Extensions; namespace Emby.Server.Implementations.Services { @@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Services if (propertySerializerEntry.PropertyType == typeof(bool)) { //InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value - propertyTextValue = LeftPart(propertyTextValue, ','); + propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString(); } var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue); @@ -95,19 +96,6 @@ namespace Emby.Server.Implementations.Services return instance; } - - public static string LeftPart(string strVal, char needle) - { - if (strVal == null) - { - return null; - } - - var pos = strVal.IndexOf(needle); - return pos == -1 - ? strVal - : strVal.Substring(0, pos); - } } internal static class TypeAccessor diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs index 5d4407f3b8..483c63ade7 100644 --- a/Emby.Server.Implementations/Services/UrlExtensions.cs +++ b/Emby.Server.Implementations/Services/UrlExtensions.cs @@ -1,4 +1,5 @@ using System; +using MediaBrowser.Common.Extensions; namespace Emby.Server.Implementations.Services { @@ -13,25 +14,12 @@ namespace Emby.Server.Implementations.Services public static string GetMethodName(this Type type) { var typeName = type.FullName != null // can be null, e.g. generic types - ? LeftPart(type.FullName, "[[") // Generic Fullname - .Replace(type.Namespace + ".", string.Empty) // Trim Namespaces - .Replace("+", ".") // Convert nested into normal type + ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname + .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces + .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type : type.Name; return type.IsGenericParameter ? "'" + typeName : typeName; } - - private static string LeftPart(string strVal, string needle) - { - if (strVal == null) - { - return null; - } - - var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase); - return pos == -1 - ? strVal - : strVal.Substring(0, pos); - } } } diff --git a/Emby.Server.Implementations/Session/HttpSessionController.cs b/Emby.Server.Implementations/Session/HttpSessionController.cs deleted file mode 100644 index dfb81816c7..0000000000 --- a/Emby.Server.Implementations/Session/HttpSessionController.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Session; - -namespace Emby.Server.Implementations.Session -{ - public class HttpSessionController : ISessionController - { - private readonly IHttpClient _httpClient; - private readonly IJsonSerializer _json; - private readonly ISessionManager _sessionManager; - - public SessionInfo Session { get; private set; } - - private readonly string _postUrl; - - public HttpSessionController(IHttpClient httpClient, - IJsonSerializer json, - SessionInfo session, - string postUrl, ISessionManager sessionManager) - { - _httpClient = httpClient; - _json = json; - Session = session; - _postUrl = postUrl; - _sessionManager = sessionManager; - } - - private string PostUrl => string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl); - - public bool IsSessionActive => (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5; - - public bool SupportsMediaControl => true; - - private Task SendMessage(string name, string messageId, CancellationToken cancellationToken) - { - return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken); - } - - private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken) - { - args["messageId"] = messageId; - var url = PostUrl + "/" + name + ToQueryString(args); - - return SendRequest(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = false - }); - } - - private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken) - { - var dict = new Dictionary<string, string>(); - - dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); - - if (command.StartPositionTicks.HasValue) - { - dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture); - } - if (command.AudioStreamIndex.HasValue) - { - dict["AudioStreamIndex"] = command.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture); - } - if (command.SubtitleStreamIndex.HasValue) - { - dict["SubtitleStreamIndex"] = command.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture); - } - if (command.StartIndex.HasValue) - { - dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture); - } - if (!string.IsNullOrEmpty(command.MediaSourceId)) - { - dict["MediaSourceId"] = command.MediaSourceId; - } - - return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken); - } - - private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken) - { - var args = new Dictionary<string, string>(); - - if (command.Command == PlaystateCommand.Seek) - { - if (!command.SeekPositionTicks.HasValue) - { - throw new ArgumentException("SeekPositionTicks cannot be null"); - } - - args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture); - } - - return SendMessage(command.Command.ToString(), messageId, args, cancellationToken); - } - - private string[] _supportedMessages = Array.Empty<string>(); - public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) - { - if (!IsSessionActive) - { - return Task.CompletedTask; - } - - if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase)) - { - return SendPlayCommand(data as PlayRequest, messageId, cancellationToken); - } - if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) - { - return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken); - } - if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) - { - var command = data as GeneralCommand; - return SendMessage(command.Name, messageId, command.Arguments, cancellationToken); - } - - if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase)) - { - return Task.CompletedTask; - } - - var url = PostUrl + "/" + name; - - url += "?messageId=" + messageId; - - var options = new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - BufferContent = false - }; - - if (data != null) - { - if (typeof(T) == typeof(string)) - { - var str = data as string; - if (!string.IsNullOrEmpty(str)) - { - options.RequestContent = str; - options.RequestContentType = "application/json"; - } - } - else - { - options.RequestContent = _json.SerializeToString(data); - options.RequestContentType = "application/json"; - } - } - - return SendRequest(options); - } - - private async Task SendRequest(HttpRequestOptions options) - { - using (var response = await _httpClient.Post(options).ConfigureAwait(false)) - { - - } - } - - private static string ToQueryString(Dictionary<string, string> nvc) - { - var array = (from item in nvc - select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value))) - .ToArray(); - - var args = string.Join("&", array); - - if (string.IsNullOrEmpty(args)) - { - return args; - } - - return "?" + args; - } - } -} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 2c8b2f29d7..611c73a2f9 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -25,6 +25,7 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session @@ -477,8 +478,7 @@ namespace Emby.Server.Implementations.Session Client = appName, DeviceId = deviceId, ApplicationVersion = appVersion, - Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture), - ServerId = _appHost.SystemId + Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; var username = user?.Name; @@ -1042,12 +1042,12 @@ namespace Emby.Server.Implementations.Session private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken) { - var controllers = session.SessionControllers.ToArray(); - var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + var controllers = session.SessionControllers; + var messageId = Guid.NewGuid(); foreach (var controller in controllers) { - await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false); + await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false); } } @@ -1055,13 +1055,13 @@ namespace Emby.Server.Implementations.Session { IEnumerable<Task> GetTasks() { - var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + var messageId = Guid.NewGuid(); foreach (var session in sessions) { var controllers = session.SessionControllers; foreach (var controller in controllers) { - yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken); + yield return controller.SendMessage(name, messageId, data, cancellationToken); } } } @@ -1154,6 +1154,22 @@ namespace Emby.Server.Implementations.Session await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); } + /// <inheritdoc /> + public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken) + { + CheckDisposed(); + var session = GetSessionToRemoteControl(sessionId); + await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false); + } + + /// <inheritdoc /> + public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken) + { + CheckDisposed(); + var session = GetSessionToRemoteControl(sessionId); + await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false); + } + private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user) { var item = _libraryManager.GetItemById(id); @@ -1432,7 +1448,7 @@ namespace Emby.Server.Implementations.Session if (user == null) { AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request)); - throw new SecurityException("Invalid username or password entered."); + throw new AuthenticationException("Invalid username or password entered."); } if (!string.IsNullOrEmpty(request.DeviceId) @@ -1780,7 +1796,7 @@ namespace Emby.Server.Implementations.Session throw new ArgumentNullException(nameof(info)); } - var user = info.UserId.Equals(Guid.Empty) + var user = info.UserId == Guid.Empty ? null : _userManager.GetUserById(info.UserId); diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 930f2d35d3..e7b4b0ec35 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -1,9 +1,13 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Events; -using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -12,8 +16,23 @@ namespace Emby.Server.Implementations.Session /// <summary> /// Class SessionWebSocketListener /// </summary> - public class SessionWebSocketListener : IWebSocketListener, IDisposable + public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable { + /// <summary> + /// The timeout in seconds after which a WebSocket is considered to be lost. + /// </summary> + public const int WebSocketLostTimeout = 60; + + /// <summary> + /// The keep-alive interval factor; controls how often the watcher will check on the status of the WebSockets. + /// </summary> + public const float IntervalFactor = 0.2f; + + /// <summary> + /// The ForceKeepAlive factor; controls when a ForceKeepAlive is sent. + /// </summary> + public const float ForceKeepAliveFactor = 0.75f; + /// <summary> /// The _session manager /// </summary> @@ -23,42 +42,62 @@ namespace Emby.Server.Implementations.Session /// The _logger /// </summary> private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + + private readonly IHttpServer _httpServer; /// <summary> - /// The _dto service + /// The KeepAlive cancellation token. /// </summary> - private readonly IJsonSerializer _json; + private CancellationTokenSource _keepAliveCancellationToken; - private readonly IHttpServer _httpServer; + /// <summary> + /// Lock used for accesing the KeepAlive cancellation token. + /// </summary> + private readonly object _keepAliveLock = new object(); + + /// <summary> + /// The WebSocket watchlist. + /// </summary> + private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>(); + /// <summary> + /// Lock used for accesing the WebSockets watchlist. + /// </summary> + private readonly object _webSocketsLock = new object(); /// <summary> /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="sessionManager">The session manager.</param> /// <param name="loggerFactory">The logger factory.</param> - /// <param name="json">The json.</param> /// <param name="httpServer">The HTTP server.</param> - public SessionWebSocketListener(ISessionManager sessionManager, ILoggerFactory loggerFactory, IJsonSerializer json, IHttpServer httpServer) + public SessionWebSocketListener( + ILogger<SessionWebSocketListener> logger, + ISessionManager sessionManager, + ILoggerFactory loggerFactory, + IHttpServer httpServer) { + _logger = logger; _sessionManager = sessionManager; - _logger = loggerFactory.CreateLogger(GetType().Name); - _json = json; + _loggerFactory = loggerFactory; _httpServer = httpServer; - httpServer.WebSocketConnected += _serverManager_WebSocketConnected; + + httpServer.WebSocketConnected += OnServerManagerWebSocketConnected; } - void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) + private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) { - var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint); - + var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString()); if (session != null) { EnsureController(session, e.Argument); + await KeepAliveWebSocket(e.Argument); } else { - _logger.LogWarning("Unable to determine session based on url: {0}", e.Argument.Url); + _logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString); } } @@ -79,9 +118,11 @@ namespace Emby.Server.Implementations.Session return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint); } + /// <inheritdoc /> public void Dispose() { - _httpServer.WebSocketConnected -= _serverManager_WebSocketConnected; + _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected; + StopKeepAlive(); } /// <summary> @@ -94,10 +135,212 @@ namespace Emby.Server.Implementations.Session private void EnsureController(SessionInfo session, IWebSocketConnection connection) { - var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager)); + var controllerInfo = session.EnsureController<WebSocketController>( + s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager)); var controller = (WebSocketController)controllerInfo.Item1; controller.AddWebSocket(connection); } + + /// <summary> + /// Called when a WebSocket is closed. + /// </summary> + /// <param name="sender">The WebSocket.</param> + /// <param name="e">The event arguments.</param> + private void OnWebSocketClosed(object sender, EventArgs e) + { + var webSocket = (IWebSocketConnection)sender; + _logger.LogDebug("WebSocket {0} is closed.", webSocket); + RemoveWebSocket(webSocket); + } + + /// <summary> + /// Adds a WebSocket to the KeepAlive watchlist. + /// </summary> + /// <param name="webSocket">The WebSocket to monitor.</param> + private async Task KeepAliveWebSocket(IWebSocketConnection webSocket) + { + lock (_webSocketsLock) + { + if (!_webSockets.Add(webSocket)) + { + _logger.LogWarning("Multiple attempts to keep alive single WebSocket {0}", webSocket); + return; + } + webSocket.Closed += OnWebSocketClosed; + webSocket.LastKeepAliveDate = DateTime.UtcNow; + + StartKeepAlive(); + } + + // Notify WebSocket about timeout + try + { + await SendForceKeepAlive(webSocket); + } + catch (WebSocketException exception) + { + _logger.LogWarning(exception, "Cannot send ForceKeepAlive message to WebSocket {0}.", webSocket); + } + } + + /// <summary> + /// Removes a WebSocket from the KeepAlive watchlist. + /// </summary> + /// <param name="webSocket">The WebSocket to remove.</param> + private void RemoveWebSocket(IWebSocketConnection webSocket) + { + lock (_webSocketsLock) + { + if (!_webSockets.Remove(webSocket)) + { + _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket); + } + else + { + webSocket.Closed -= OnWebSocketClosed; + } + } + } + + /// <summary> + /// Starts the KeepAlive watcher. + /// </summary> + private void StartKeepAlive() + { + lock (_keepAliveLock) + { + if (_keepAliveCancellationToken == null) + { + _keepAliveCancellationToken = new CancellationTokenSource(); + // Start KeepAlive watcher + _ = RepeatAsyncCallbackEvery( + KeepAliveSockets, + TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor), + _keepAliveCancellationToken.Token); + } + } + } + + /// <summary> + /// Stops the KeepAlive watcher. + /// </summary> + private void StopKeepAlive() + { + lock (_keepAliveLock) + { + if (_keepAliveCancellationToken != null) + { + _keepAliveCancellationToken.Cancel(); + _keepAliveCancellationToken = null; + } + } + + lock (_webSocketsLock) + { + foreach (var webSocket in _webSockets) + { + webSocket.Closed -= OnWebSocketClosed; + } + + _webSockets.Clear(); + } + } + + /// <summary> + /// Checks status of KeepAlive of WebSockets. + /// </summary> + private async Task KeepAliveSockets() + { + List<IWebSocketConnection> inactive; + List<IWebSocketConnection> lost; + + lock (_webSocketsLock) + { + _logger.LogDebug("Watching {0} WebSockets.", _webSockets.Count); + + inactive = _webSockets.Where(i => + { + var elapsed = (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds; + return (elapsed > WebSocketLostTimeout * ForceKeepAliveFactor) && (elapsed < WebSocketLostTimeout); + }).ToList(); + lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList(); + } + + if (inactive.Any()) + { + _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count); + } + + foreach (var webSocket in inactive) + { + try + { + await SendForceKeepAlive(webSocket); + } + catch (WebSocketException exception) + { + _logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket."); + lost.Add(webSocket); + } + } + + lock (_webSocketsLock) + { + if (lost.Any()) + { + _logger.LogInformation("Lost {0} WebSockets.", lost.Count); + foreach (var webSocket in lost) + { + // TODO: handle session relative to the lost webSocket + RemoveWebSocket(webSocket); + } + } + + if (!_webSockets.Any()) + { + StopKeepAlive(); + } + } + } + + /// <summary> + /// Sends a ForceKeepAlive message to a WebSocket. + /// </summary> + /// <param name="webSocket">The WebSocket.</param> + /// <returns>Task.</returns> + private Task SendForceKeepAlive(IWebSocketConnection webSocket) + { + return webSocket.SendAsync(new WebSocketMessage<int> + { + MessageType = "ForceKeepAlive", + Data = WebSocketLostTimeout + }, CancellationToken.None); + } + + /// <summary> + /// Runs a given async callback once every specified interval time, until cancelled. + /// </summary> + /// <param name="callback">The async callback.</param> + /// <param name="interval">The interval time.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task RepeatAsyncCallbackEvery(Func<Task> callback, TimeSpan interval, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await callback(); + Task task = Task.Delay(interval, cancellationToken); + + try + { + await task; + } + catch (TaskCanceledException) + { + return; + } + } + } } } diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index 0d483c55fa..a0274acd20 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -1,3 +1,7 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session { - public class WebSocketController : ISessionController, IDisposable + public sealed class WebSocketController : ISessionController, IDisposable { - public SessionInfo Session { get; private set; } - public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; } - private readonly ILogger _logger; - private readonly ISessionManager _sessionManager; + private readonly SessionInfo _session; - public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager) + private readonly List<IWebSocketConnection> _sockets; + private bool _disposed = false; + + public WebSocketController( + ILogger<WebSocketController> logger, + SessionInfo session, + ISessionManager sessionManager) { - Session = session; _logger = logger; + _session = session; _sessionManager = sessionManager; - Sockets = new List<IWebSocketConnection>(); + _sockets = new List<IWebSocketConnection>(); } private bool HasOpenSockets => GetActiveSockets().Any(); + /// <inheritdoc /> public bool SupportsMediaControl => HasOpenSockets; + /// <inheritdoc /> public bool IsSessionActive => HasOpenSockets; private IEnumerable<IWebSocketConnection> GetActiveSockets() - { - return Sockets - .OrderByDescending(i => i.LastActivityDate) - .Where(i => i.State == WebSocketState.Open); - } + => _sockets.Where(i => i.State == WebSocketState.Open); public void AddWebSocket(IWebSocketConnection connection) { - var sockets = Sockets.ToList(); - sockets.Add(connection); + _logger.LogDebug("Adding websocket to session {Session}", _session.Id); + _sockets.Add(connection); - Sockets = sockets; - - connection.Closed += connection_Closed; + connection.Closed += OnConnectionClosed; } - void connection_Closed(object sender, EventArgs e) + private void OnConnectionClosed(object sender, EventArgs e) { var connection = (IWebSocketConnection)sender; - var sockets = Sockets.ToList(); - sockets.Remove(connection); - - Sockets = sockets; - - _sessionManager.CloseIfNeeded(Session); + _logger.LogDebug("Removing websocket from session {Session}", _session.Id); + _sockets.Remove(connection); + connection.Closed -= OnConnectionClosed; + _sessionManager.CloseIfNeeded(_session); } - public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendMessage<T>( + string name, + Guid messageId, + T data, + CancellationToken cancellationToken) { var socket = GetActiveSockets() + .OrderByDescending(i => i.LastActivityDate) .FirstOrDefault(); if (socket == null) @@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session return Task.CompletedTask; } - return socket.SendAsync(new WebSocketMessage<T> - { - Data = data, - MessageType = name, - MessageId = messageId - - }, cancellationToken); + return socket.SendAsync( + new WebSocketMessage<T> + { + Data = data, + MessageType = name, + MessageId = messageId + }, + cancellationToken); } + /// <inheritdoc /> public void Dispose() { - foreach (var socket in Sockets.ToList()) + if (_disposed) { - socket.Closed -= connection_Closed; + return; } + + foreach (var socket in _sockets) + { + socket.Closed -= OnConnectionClosed; + } + + _disposed = true; } } } diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs deleted file mode 100644 index 67521d6c63..0000000000 --- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Net; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class SharpWebSocket : IWebSocket - { - /// <summary> - /// The logger - /// </summary> - private readonly ILogger _logger; - - public event EventHandler<EventArgs> Closed; - - /// <summary> - /// Gets or sets the web socket. - /// </summary> - /// <value>The web socket.</value> - private readonly WebSocket _webSocket; - - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private bool _disposed; - - public SharpWebSocket(WebSocket socket, ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _webSocket = socket ?? throw new ArgumentNullException(nameof(socket)); - } - - /// <summary> - /// Gets the state. - /// </summary> - /// <value>The state.</value> - public WebSocketState State => _webSocket.State; - - /// <summary> - /// Sends the async. - /// </summary> - /// <param name="bytes">The bytes.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) - { - return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken); - } - - /// <summary> - /// Sends the asynchronous. - /// </summary> - /// <param name="text">The text.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) - { - return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken); - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (_disposed) - { - return; - } - - if (dispose) - { - _cancellationTokenSource.Cancel(); - if (_webSocket.State == WebSocketState.Open) - { - _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client", - CancellationToken.None); - } - Closed?.Invoke(this, EventArgs.Empty); - } - - _disposed = true; - } - - /// <summary> - /// Gets or sets the receive action. - /// </summary> - /// <value>The receive action.</value> - public Action<byte[]> OnReceiveBytes { get; set; } - } -} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs deleted file mode 100644 index b85750c9b9..0000000000 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using Emby.Server.Implementations.Net; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class WebSocketSharpListener : IHttpListener - { - private readonly ILogger _logger; - - private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); - private CancellationToken _disposeCancellationToken; - - public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger) - { - _logger = logger; - _disposeCancellationToken = _disposeCancellationTokenSource.Token; - } - - public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; } - - public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; } - - public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; } - - private static void LogRequest(ILogger logger, HttpRequest request) - { - var url = request.GetDisplayUrl(); - - logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString()); - } - - public async Task ProcessWebSocketRequest(HttpContext ctx) - { - try - { - LogRequest(_logger, ctx.Request); - var endpoint = ctx.Connection.RemoteIpAddress.ToString(); - var url = ctx.Request.GetDisplayUrl(); - - var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false); - var socket = new SharpWebSocket(webSocketContext, _logger); - - WebSocketConnected(new WebSocketConnectEventArgs - { - Url = url, - QueryString = ctx.Request.Query, - WebSocket = socket, - Endpoint = endpoint - }); - - WebSocketReceiveResult result; - var message = new List<byte>(); - - do - { - var buffer = WebSocket.CreateServerBuffer(4096); - result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken); - message.AddRange(buffer.Array.Take(result.Count)); - - if (result.EndOfMessage) - { - socket.OnReceiveBytes(message.ToArray()); - message.Clear(); - } - } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close); - - - if (webSocketContext.State == WebSocketState.Open) - { - await webSocketContext.CloseAsync( - result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, - result.CloseStatusDescription, - _disposeCancellationToken).ConfigureAwait(false); - } - - socket.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "AcceptWebSocketAsync error"); - if (!ctx.Response.HasStarted) - { - ctx.Response.StatusCode = 500; - } - } - } - - public Task Stop() - { - _disposeCancellationTokenSource.Cancel(); - return Task.CompletedTask; - } - - /// <summary> - /// Releases the unmanaged resources and disposes of the managed resources used. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private bool _disposed; - - /// <summary> - /// Releases the unmanaged resources and disposes of the managed resources used. - /// </summary> - /// <param name="disposing">Whether or not the managed resources should be disposed.</param> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - Stop().GetAwaiter().GetResult(); - } - - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 1781df8b51..ee5131c1ff 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Mime; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -62,6 +63,9 @@ namespace Emby.Server.Implementations.SocketSharp if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip)) { ip = Request.HttpContext.Connection.RemoteIpAddress; + + // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) + ip ??= IPAddress.Loopback; } } @@ -89,7 +93,10 @@ namespace Emby.Server.Implementations.SocketSharp public IQueryCollection QueryString => Request.Query; - public bool IsLocal => Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress); + public bool IsLocal => + (Request.HttpContext.Connection.LocalIpAddress == null + && Request.HttpContext.Connection.RemoteIpAddress == null) + || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress); public string HttpMethod => Request.Method; @@ -216,14 +223,14 @@ namespace Emby.Server.Implementations.SocketSharp pi = pi.Slice(1); } - format = LeftPart(pi, '/'); + format = pi.LeftPart('/'); if (format.Length > FormatMaxLength) { return null; } } - format = LeftPart(format, '.'); + format = format.LeftPart('.'); if (format.Contains("json", StringComparison.OrdinalIgnoreCase)) { return "application/json"; @@ -235,16 +242,5 @@ namespace Emby.Server.Implementations.SocketSharp return null; } - - public static ReadOnlySpan<char> LeftPart(ReadOnlySpan<char> strVal, char needle) - { - if (strVal == null) - { - return null; - } - - var pos = strVal.IndexOf(needle); - return pos == -1 ? strVal : strVal.Slice(0, pos); - } } } diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs new file mode 100644 index 0000000000..d430d4d162 --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -0,0 +1,517 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace Emby.Server.Implementations.SyncPlay +{ + /// <summary> + /// Class SyncPlayController. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public class SyncPlayController : ISyncPlayController + { + /// <summary> + /// Used to filter the sessions of a group. + /// </summary> + private enum BroadcastType + { + /// <summary> + /// All sessions will receive the message. + /// </summary> + AllGroup = 0, + /// <summary> + /// Only the specified session will receive the message. + /// </summary> + CurrentSession = 1, + /// <summary> + /// All sessions, except the current one, will receive the message. + /// </summary> + AllExceptCurrentSession = 2, + /// <summary> + /// Only sessions that are not buffering will receive the message. + /// </summary> + AllReady = 3 + } + + /// <summary> + /// The session manager. + /// </summary> + private readonly ISessionManager _sessionManager; + + /// <summary> + /// The SyncPlay manager. + /// </summary> + private readonly ISyncPlayManager _syncPlayManager; + + /// <summary> + /// The group to manage. + /// </summary> + private readonly GroupInfo _group = new GroupInfo(); + + /// <inheritdoc /> + public Guid GetGroupId() => _group.GroupId; + + /// <inheritdoc /> + public Guid GetPlayingItemId() => _group.PlayingItem.Id; + + /// <inheritdoc /> + public bool IsGroupEmpty() => _group.IsEmpty(); + + public SyncPlayController( + ISessionManager sessionManager, + ISyncPlayManager syncPlayManager) + { + _sessionManager = sessionManager; + _syncPlayManager = syncPlayManager; + } + + /// <summary> + /// Converts DateTime to UTC string. + /// </summary> + /// <param name="date">The date to convert.</param> + /// <value>The UTC string.</value> + private string DateToUTCString(DateTime date) + { + return date.ToUniversalTime().ToString("o"); + } + + /// <summary> + /// Filters sessions of this group. + /// </summary> + /// <param name="from">The current session.</param> + /// <param name="type">The filtering type.</param> + /// <value>The array of sessions matching the filter.</value> + private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type) + { + switch (type) + { + case BroadcastType.CurrentSession: + return new SessionInfo[] { from }; + case BroadcastType.AllGroup: + return _group.Participants.Values.Select( + session => session.Session + ).ToArray(); + case BroadcastType.AllExceptCurrentSession: + return _group.Participants.Values.Select( + session => session.Session + ).Where( + session => !session.Id.Equals(from.Id) + ).ToArray(); + case BroadcastType.AllReady: + return _group.Participants.Values.Where( + session => !session.IsBuffering + ).Select( + session => session.Session + ).ToArray(); + default: + return Array.Empty<SessionInfo>(); + } + } + + /// <summary> + /// Sends a GroupUpdate message to the interested sessions. + /// </summary> + /// <param name="from">The current session.</param> + /// <param name="type">The filtering type.</param> + /// <param name="message">The message to send.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The task.</value> + private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken) + { + IEnumerable<Task> GetTasks() + { + SessionInfo[] sessions = FilterSessions(from, type); + foreach (var session in sessions) + { + yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken); + } + } + + return Task.WhenAll(GetTasks()); + } + + /// <summary> + /// Sends a playback command to the interested sessions. + /// </summary> + /// <param name="from">The current session.</param> + /// <param name="type">The filtering type.</param> + /// <param name="message">The message to send.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The task.</value> + private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken) + { + IEnumerable<Task> GetTasks() + { + SessionInfo[] sessions = FilterSessions(from, type); + foreach (var session in sessions) + { + yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken); + } + } + + return Task.WhenAll(GetTasks()); + } + + /// <summary> + /// Builds a new playback command with some default values. + /// </summary> + /// <param name="type">The command type.</param> + /// <value>The SendCommand.</value> + private SendCommand NewSyncPlayCommand(SendCommandType type) + { + return new SendCommand() + { + GroupId = _group.GroupId.ToString(), + Command = type, + PositionTicks = _group.PositionTicks, + When = DateToUTCString(_group.LastActivity), + EmittedAt = DateToUTCString(DateTime.UtcNow) + }; + } + + /// <summary> + /// Builds a new group update message. + /// </summary> + /// <param name="type">The update type.</param> + /// <param name="data">The data to send.</param> + /// <value>The GroupUpdate.</value> + private GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data) + { + return new GroupUpdate<T>() + { + GroupId = _group.GroupId.ToString(), + Type = type, + Data = data + }; + } + + /// <inheritdoc /> + public void InitGroup(SessionInfo session, CancellationToken cancellationToken) + { + _group.AddSession(session); + _syncPlayManager.AddSessionToGroup(session, this); + + _group.PlayingItem = session.FullNowPlayingItem; + _group.IsPaused = true; + _group.PositionTicks = session.PlayState.PositionTicks ?? 0; + _group.LastActivity = DateTime.UtcNow; + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); + SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); + } + + /// <inheritdoc /> + public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) + { + if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id) + { + _group.AddSession(session); + _syncPlayManager.AddSessionToGroup(session, this); + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); + SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); + SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + + // Client join and play, syncing will happen client side + if (!_group.IsPaused) + { + var playCommand = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken); + } + else + { + var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); + } + } + else + { + var playRequest = new PlayRequest(); + playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id }; + playRequest.StartPositionTicks = _group.PositionTicks; + var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); + SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken); + } + } + + /// <inheritdoc /> + public void SessionLeave(SessionInfo session, CancellationToken cancellationToken) + { + _group.RemoveSession(session); + _syncPlayManager.RemoveSessionFromGroup(session, this); + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks); + SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); + SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + } + + /// <inheritdoc /> + public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + // The server's job is to mantain a consistent state to which clients refer to, + // as also to notify clients of state changes. + // The actual syncing of media playback happens client side. + // Clients are aware of the server's time and use it to sync. + switch (request.Type) + { + case PlaybackRequestType.Play: + HandlePlayRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.Pause: + HandlePauseRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.Seek: + HandleSeekRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.Buffering: + HandleBufferingRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.BufferingDone: + HandleBufferingDoneRequest(session, request, cancellationToken); + break; + case PlaybackRequestType.UpdatePing: + HandlePingUpdateRequest(session, request); + break; + } + } + + /// <summary> + /// Handles a play action requested by a session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The play action.</param> + /// <param name="cancellationToken">The cancellation token.</param> + private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (_group.IsPaused) + { + // Pick a suitable time that accounts for latency + var delay = _group.GetHighestPing() * 2; + delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + + // Unpause group and set starting point in future + // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) + // The added delay does not guarantee, of course, that the command will be received in time + // Playback synchronization will mainly happen client side + _group.IsPaused = false; + _group.LastActivity = DateTime.UtcNow.AddMilliseconds( + delay + ); + + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// <summary> + /// Handles a pause action requested by a session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The pause action.</param> + /// <param name="cancellationToken">The cancellation token.</param> + private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (!_group.IsPaused) + { + // Pause group and compute the media playback position + _group.IsPaused = true; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - _group.LastActivity; + _group.LastActivity = currentTime; + // Seek only if playback actually started + // (a pause request may be issued during the delay added to account for latency) + _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// <summary> + /// Handles a seek action requested by a session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The seek action.</param> + /// <param name="cancellationToken">The cancellation token.</param> + private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + // Sanitize PositionTicks + var ticks = SanitizePositionTicks(request.PositionTicks); + + // Pause and seek + _group.IsPaused = true; + _group.PositionTicks = ticks; + _group.LastActivity = DateTime.UtcNow; + + var command = NewSyncPlayCommand(SendCommandType.Seek); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + + /// <summary> + /// Handles a buffering action requested by a session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The buffering action.</param> + /// <param name="cancellationToken">The cancellation token.</param> + private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (!_group.IsPaused) + { + // Pause group and compute the media playback position + _group.IsPaused = true; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - _group.LastActivity; + _group.LastActivity = currentTime; + _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + _group.SetBuffering(session, true); + + // Send pause command to all non-buffering sessions + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.AllReady, command, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName); + SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// <summary> + /// Handles a buffering-done action requested by a session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The buffering-done action.</param> + /// <param name="cancellationToken">The cancellation token.</param> + private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + if (_group.IsPaused) + { + _group.SetBuffering(session, false); + + var requestTicks = SanitizePositionTicks(request.PositionTicks); + + var when = request.When ?? DateTime.UtcNow; + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - when; + var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; + var delay = _group.PositionTicks - clientPosition.Ticks; + + if (_group.IsBuffering()) + { + // Others are still buffering, tell this client to pause when ready + var command = NewSyncPlayCommand(SendCommandType.Pause); + var pauseAtTime = currentTime.AddMilliseconds(delay); + command.When = DateToUTCString(pauseAtTime); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + else + { + // Let other clients resume as soon as the buffering client catches up + _group.IsPaused = false; + + if (delay > _group.GetHighestPing() * 2) + { + // Client that was buffering is recovering, notifying others to resume + _group.LastActivity = currentTime.AddMilliseconds( + delay + ); + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken); + } + else + { + // Client, that was buffering, resumed playback but did not update others in time + delay = _group.GetHighestPing() * 2; + delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + + _group.LastActivity = currentTime.AddMilliseconds( + delay + ); + + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); + } + } + } + else + { + // Group was not waiting, make sure client has latest state + var command = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); + } + } + + /// <summary> + /// Sanitizes the PositionTicks, considers the current playing item when available. + /// </summary> + /// <param name="positionTicks">The PositionTicks.</param> + /// <value>The sanitized PositionTicks.</value> + private long SanitizePositionTicks(long? positionTicks) + { + var ticks = positionTicks ?? 0; + ticks = ticks >= 0 ? ticks : 0; + if (_group.PlayingItem != null) + { + var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0; + ticks = ticks > runTimeTicks ? runTimeTicks : ticks; + } + + return ticks; + } + + /// <summary> + /// Updates ping of a session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The update.</param> + private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) + { + // Collected pings are used to account for network latency when unpausing playback + _group.UpdatePing(session, request.Ping ?? _group.DefaulPing); + } + + /// <inheritdoc /> + public GroupInfoView GetInfo() + { + return new GroupInfoView() + { + GroupId = GetGroupId().ToString(), + PlayingItemName = _group.PlayingItem.Name, + PlayingItemId = _group.PlayingItem.Id.ToString(), + PositionTicks = _group.PositionTicks, + Participants = _group.Participants.Values.Select(session => session.Session.UserName).Distinct().ToList() + }; + } + } +} diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs new file mode 100644 index 0000000000..1f76dd4e36 --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -0,0 +1,398 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Logging; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.SyncPlay; + +namespace Emby.Server.Implementations.SyncPlay +{ + /// <summary> + /// Class SyncPlayManager. + /// </summary> + public class SyncPlayManager : ISyncPlayManager, IDisposable + { + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger _logger; + + /// <summary> + /// The user manager. + /// </summary> + private readonly IUserManager _userManager; + + /// <summary> + /// The session manager. + /// </summary> + private readonly ISessionManager _sessionManager; + + /// <summary> + /// The library manager. + /// </summary> + private readonly ILibraryManager _libraryManager; + + /// <summary> + /// The map between sessions and groups. + /// </summary> + private readonly Dictionary<string, ISyncPlayController> _sessionToGroupMap = + new Dictionary<string, ISyncPlayController>(StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// The groups. + /// </summary> + private readonly Dictionary<Guid, ISyncPlayController> _groups = + new Dictionary<Guid, ISyncPlayController>(); + + /// <summary> + /// Lock used for accesing any group. + /// </summary> + private readonly object _groupsLock = new object(); + + private bool _disposed = false; + + public SyncPlayManager( + ILogger<SyncPlayManager> logger, + IUserManager userManager, + ISessionManager sessionManager, + ILibraryManager libraryManager) + { + _logger = logger; + _userManager = userManager; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + + _sessionManager.SessionEnded += OnSessionManagerSessionEnded; + _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped; + } + + /// <summary> + /// Gets all groups. + /// </summary> + /// <value>All groups.</value> + public IEnumerable<ISyncPlayController> Groups => _groups.Values; + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and optionally managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; + _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; + + _disposed = true; + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) + { + var session = e.SessionInfo; + if (!IsSessionInGroup(session)) + { + return; + } + + LeaveGroup(session, CancellationToken.None); + } + + private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e) + { + var session = e.Session; + if (!IsSessionInGroup(session)) + { + return; + } + + LeaveGroup(session, CancellationToken.None); + } + + private bool IsSessionInGroup(SessionInfo session) + { + return _sessionToGroupMap.ContainsKey(session.Id); + } + + private bool HasAccessToItem(User user, Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + + // Check ParentalRating access + var hasParentalRatingAccess = true; + if (user.Policy.MaxParentalRating.HasValue) + { + hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating; + } + + if (!user.Policy.EnableAllFolders && hasParentalRatingAccess) + { + var collections = _libraryManager.GetCollectionFolders(item).Select( + folder => folder.Id.ToString("N", CultureInfo.InvariantCulture) + ); + var intersect = collections.Intersect(user.Policy.EnabledFolders); + return intersect.Any(); + } + else + { + return hasParentalRatingAccess; + } + } + + private Guid? GetSessionGroup(SessionInfo session) + { + ISyncPlayController group; + _sessionToGroupMap.TryGetValue(session.Id, out group); + if (group != null) + { + return group.GetGroupId(); + } + else + { + return null; + } + } + + /// <inheritdoc /> + public void NewGroup(SessionInfo session, CancellationToken cancellationToken) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups) + { + _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); + + var error = new GroupUpdate<string>() + { + Type = GroupUpdateType.CreateGroupDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + lock (_groupsLock) + { + if (IsSessionInGroup(session)) + { + LeaveGroup(session, cancellationToken); + } + + var group = new SyncPlayController(_sessionManager, this); + _groups[group.GetGroupId()] = group; + + group.InitGroup(session, cancellationToken); + } + } + + /// <inheritdoc /> + public void JoinGroup(SessionInfo session, Guid groupId, JoinGroupRequest request, CancellationToken cancellationToken) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) + { + _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id); + + var error = new GroupUpdate<string>() + { + Type = GroupUpdateType.JoinGroupDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + lock (_groupsLock) + { + ISyncPlayController group; + _groups.TryGetValue(groupId, out group); + + if (group == null) + { + _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId); + + var error = new GroupUpdate<string>() + { + Type = GroupUpdateType.GroupDoesNotExist + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + if (!HasAccessToItem(user, group.GetPlayingItemId())) + { + _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId()); + + var error = new GroupUpdate<string>() + { + GroupId = group.GetGroupId().ToString(), + Type = GroupUpdateType.LibraryAccessDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + if (IsSessionInGroup(session)) + { + if (GetSessionGroup(session).Equals(groupId)) + { + return; + } + + LeaveGroup(session, cancellationToken); + } + + group.SessionJoin(session, request, cancellationToken); + } + } + + /// <inheritdoc /> + public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken) + { + // TODO: determine what happens to users that are in a group and get their permissions revoked + lock (_groupsLock) + { + ISyncPlayController group; + _sessionToGroupMap.TryGetValue(session.Id, out group); + + if (group == null) + { + _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id); + + var error = new GroupUpdate<string>() + { + Type = GroupUpdateType.NotInGroup + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + group.SessionLeave(session, cancellationToken); + + if (group.IsGroupEmpty()) + { + _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId()); + _groups.Remove(group.GetGroupId(), out _); + } + } + } + + /// <inheritdoc /> + public List<GroupInfoView> ListGroups(SessionInfo session, Guid filterItemId) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) + { + return new List<GroupInfoView>(); + } + + // Filter by item if requested + if (!filterItemId.Equals(Guid.Empty)) + { + return _groups.Values.Where( + group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId()) + ).Select( + group => group.GetInfo() + ).ToList(); + } + // Otherwise show all available groups + else + { + return _groups.Values.Where( + group => HasAccessToItem(user, group.GetPlayingItemId()) + ).Select( + group => group.GetInfo() + ).ToList(); + } + } + + /// <inheritdoc /> + public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + { + var user = _userManager.GetUserById(session.UserId); + + if (user.Policy.SyncPlayAccess == SyncPlayAccess.None) + { + _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id); + + var error = new GroupUpdate<string>() + { + Type = GroupUpdateType.JoinGroupDenied + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + lock (_groupsLock) + { + ISyncPlayController group; + _sessionToGroupMap.TryGetValue(session.Id, out group); + + if (group == null) + { + _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id); + + var error = new GroupUpdate<string>() + { + Type = GroupUpdateType.NotInGroup + }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None); + return; + } + + group.HandleRequest(session, request, cancellationToken); + } + } + + /// <inheritdoc /> + public void AddSessionToGroup(SessionInfo session, ISyncPlayController group) + { + if (IsSessionInGroup(session)) + { + throw new InvalidOperationException("Session in other group already!"); + } + + _sessionToGroupMap[session.Id] = group; + } + + /// <inheritdoc /> + public void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group) + { + if (!IsSessionInGroup(session)) + { + throw new InvalidOperationException("Session not in any group!"); + } + + ISyncPlayController tempGroup; + _sessionToGroupMap.Remove(session.Id, out tempGroup); + + if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) + { + throw new InvalidOperationException("Session was in wrong group!"); + } + } + } +} diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index c91d137a72..a26f714b12 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller; using MediaBrowser.Model.ApiClient; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Udp @@ -21,6 +22,12 @@ namespace Emby.Server.Implementations.Udp /// </summary> private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _config; + + /// <summary> + /// Address Override Configuration Key. + /// </summary> + public const string AddressOverrideConfigKey = "PublishedServerUrl"; private Socket _udpSocket; private IPEndPoint _endpoint; @@ -31,15 +38,18 @@ namespace Emby.Server.Implementations.Udp /// <summary> /// Initializes a new instance of the <see cref="UdpServer" /> class. /// </summary> - public UdpServer(ILogger logger, IServerApplicationHost appHost) + public UdpServer(ILogger logger, IServerApplicationHost appHost, IConfiguration configuration) { _logger = logger; _appHost = appHost; + _config = configuration; } private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken) { - var localUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey]) + ? _config[AddressOverrideConfigKey] + : await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(localUrl)) { @@ -105,7 +115,7 @@ namespace Emby.Server.Implementations.Udp } catch (SocketException ex) { - _logger.LogError(ex, "Failed to receive data drom socket"); + _logger.LogError(ex, "Failed to receive data from socket"); } catch (OperationCanceledException) { diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 25f70471ac..0b2309889f 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -26,7 +26,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Updates { /// <summary> - /// Manages all install, uninstall and update operations (both plugins and system). + /// Manages all install, uninstall, and update operations for the system and individual plugins. /// </summary> public class InstallationManager : IInstallationManager { @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Updates public const string PluginManifestUrlKey = "InstallationManager:PluginManifestUrl"; /// <summary> - /// The _logger. + /// The logger. /// </summary> private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; @@ -112,10 +112,10 @@ namespace Emby.Server.Implementations.Updates public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled; /// <inheritdoc /> - public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated; + public event EventHandler<GenericEventArgs<(IPlugin, VersionInfo)>> PluginUpdated; /// <inheritdoc /> - public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled; + public event EventHandler<GenericEventArgs<VersionInfo>> PluginInstalled; /// <inheritdoc /> public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; @@ -183,61 +183,56 @@ namespace Emby.Server.Implementations.Updates } /// <inheritdoc /> - public IEnumerable<PackageVersionInfo> GetCompatibleVersions( - IEnumerable<PackageVersionInfo> availableVersions, - Version minVersion = null, - PackageVersionClass classification = PackageVersionClass.Release) + public IEnumerable<VersionInfo> GetCompatibleVersions( + IEnumerable<VersionInfo> availableVersions, + Version minVersion = null) { var appVer = _applicationHost.ApplicationVersion; availableVersions = availableVersions - .Where(x => x.classification == classification - && Version.Parse(x.requiredVersionStr) <= appVer); + .Where(x => Version.Parse(x.targetAbi) <= appVer); if (minVersion != null) { - availableVersions = availableVersions.Where(x => x.Version >= minVersion); + availableVersions = availableVersions.Where(x => x.version >= minVersion); } - return availableVersions.OrderByDescending(x => x.Version); + return availableVersions.OrderByDescending(x => x.version); } /// <inheritdoc /> - public IEnumerable<PackageVersionInfo> GetCompatibleVersions( + public IEnumerable<VersionInfo> GetCompatibleVersions( IEnumerable<PackageInfo> availablePackages, string name = null, Guid guid = default, - Version minVersion = null, - PackageVersionClass classification = PackageVersionClass.Release) + Version minVersion = null) { var package = FilterPackages(availablePackages, name, guid).FirstOrDefault(); - // Package not found. + // Package not found in repository if (package == null) { - return Enumerable.Empty<PackageVersionInfo>(); + return Enumerable.Empty<VersionInfo>(); } return GetCompatibleVersions( package.versions, - minVersion, - classification); + minVersion); } /// <inheritdoc /> - public async Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default) + public async Task<IEnumerable<VersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default) { var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false); return GetAvailablePluginUpdates(catalog); } - private IEnumerable<PackageVersionInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog) + private IEnumerable<VersionInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog) { foreach (var plugin in _applicationHost.Plugins) { - var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version, _applicationHost.SystemUpdateLevel); - var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version); - if (version != null - && !CompletedInstallations.Any(x => string.Equals(x.AssemblyGuid, version.guid, StringComparison.OrdinalIgnoreCase))) + var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version); + var version = compatibleversions.FirstOrDefault(y => y.version > plugin.Version); + if (version != null && !CompletedInstallations.Any(x => string.Equals(x.Guid, version.guid, StringComparison.OrdinalIgnoreCase))) { yield return version; } @@ -245,7 +240,7 @@ namespace Emby.Server.Implementations.Updates } /// <inheritdoc /> - public async Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken) + public async Task InstallPackage(VersionInfo package, CancellationToken cancellationToken) { if (package == null) { @@ -254,11 +249,9 @@ namespace Emby.Server.Implementations.Updates var installationInfo = new InstallationInfo { - Id = Guid.NewGuid(), + Guid = package.guid, Name = package.name, - AssemblyGuid = package.guid, - UpdateClass = package.classification, - Version = package.versionStr + Version = package.version.ToString() }; var innerCancellationTokenSource = new CancellationTokenSource(); @@ -276,7 +269,7 @@ namespace Emby.Server.Implementations.Updates var installationEventArgs = new InstallationEventArgs { InstallationInfo = installationInfo, - PackageVersionInfo = package + VersionInfo = package }; PackageInstalling?.Invoke(this, installationEventArgs); @@ -301,7 +294,7 @@ namespace Emby.Server.Implementations.Updates _currentInstallations.Remove(tuple); } - _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionStr); + _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.version); PackageInstallationCancelled?.Invoke(this, installationEventArgs); @@ -337,7 +330,7 @@ namespace Emby.Server.Implementations.Updates /// <param name="package">The package.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns><see cref="Task" />.</returns> - private async Task InstallPackageInternal(PackageVersionInfo package, CancellationToken cancellationToken) + private async Task InstallPackageInternal(VersionInfo package, CancellationToken cancellationToken) { // Set last update time if we were installed before IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase)) @@ -349,26 +342,26 @@ namespace Emby.Server.Implementations.Updates // Do plugin-specific processing if (plugin == null) { - _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); + _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.version); - PluginInstalled?.Invoke(this, new GenericEventArgs<PackageVersionInfo>(package)); + PluginInstalled?.Invoke(this, new GenericEventArgs<VersionInfo>(package)); } else { - _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); + _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.version); - PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, PackageVersionInfo)>((plugin, package))); + PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, VersionInfo)>((plugin, package))); } _applicationHost.NotifyPendingRestart(); } - private async Task PerformPackageInstallation(PackageVersionInfo package, CancellationToken cancellationToken) + private async Task PerformPackageInstallation(VersionInfo package, CancellationToken cancellationToken) { - var extension = Path.GetExtension(package.targetFilename); + var extension = Path.GetExtension(package.filename); if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) { - _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename); + _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.filename); return; } @@ -415,7 +408,7 @@ namespace Emby.Server.Implementations.Updates } /// <summary> - /// Uninstalls a plugin + /// Uninstalls a plugin. /// </summary> /// <param name="plugin">The plugin.</param> public void UninstallPlugin(IPlugin plugin) @@ -473,7 +466,7 @@ namespace Emby.Server.Implementations.Updates { lock (_currentInstallationsLock) { - var install = _currentInstallations.Find(x => x.info.Id == id); + var install = _currentInstallations.Find(x => x.info.Guid == id.ToString()); if (install == default((InstallationInfo, CancellationTokenSource))) { return false; diff --git a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs deleted file mode 100644 index eb18774408..0000000000 --- a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Model.Net; - -namespace Emby.Server.Implementations.WebSockets -{ - public interface IWebSocketHandler - { - Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource); - } -} diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs deleted file mode 100644 index 31a7468fbc..0000000000 --- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; -using UtfUnknown; - -namespace Emby.Server.Implementations.WebSockets -{ - public class WebSocketManager - { - private readonly IWebSocketHandler[] _webSocketHandlers; - private readonly IJsonSerializer _jsonSerializer; - private readonly ILogger<WebSocketManager> _logger; - private const int BufferSize = 4096; - - public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger) - { - _webSocketHandlers = webSocketHandlers; - _jsonSerializer = jsonSerializer; - _logger = logger; - } - - public async Task OnWebSocketConnected(WebSocket webSocket) - { - var taskCompletionSource = new TaskCompletionSource<bool>(); - var cancellationToken = new CancellationTokenSource().Token; - WebSocketReceiveResult result; - var message = new List<byte>(); - - // Keep listening for incoming messages, otherwise the socket closes automatically - do - { - var buffer = WebSocket.CreateServerBuffer(BufferSize); - result = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); - message.AddRange(buffer.Array.Take(result.Count)); - - if (result.EndOfMessage) - { - await ProcessMessage(message.ToArray(), taskCompletionSource).ConfigureAwait(false); - message.Clear(); - } - } while (!taskCompletionSource.Task.IsCompleted && - webSocket.State == WebSocketState.Open && - result.MessageType != WebSocketMessageType.Close); - - if (webSocket.State == WebSocketState.Open) - { - await webSocket.CloseAsync( - result.CloseStatus ?? WebSocketCloseStatus.NormalClosure, - result.CloseStatusDescription, - cancellationToken).ConfigureAwait(false); - } - } - - private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource) - { - var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName; - var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase) - ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length) - : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length); - - // All messages are expected to be valid JSON objects - if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message); - return; - } - - try - { - var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message); - - _logger.LogDebug("Websocket message received: {0}", info.MessageType); - - var tasks = _webSocketHandlers.Select(handler => Task.Run(() => - { - try - { - handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}", - handler.GetType().Name, info.MessageType ?? string.Empty); - } - })); - - await Task.WhenAll(tasks); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing web socket message"); - } - } - } -} diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 26f7d9d2dd..aab1141ee8 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -1,3 +1,4 @@ +using System.Security.Authentication; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; @@ -59,6 +60,10 @@ namespace Jellyfin.Api.Auth return Task.FromResult(AuthenticateResult.Success(ticket)); } + catch (AuthenticationException ex) + { + return Task.FromResult(AuthenticateResult.Fail(ex)); + } catch (SecurityException ex) { return Task.FromResult(AuthenticateResult.Fail(ex)); diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index 1f4508e6cb..a34f9eb62f 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,3 +1,4 @@ +using System.Net.Mime; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api @@ -7,6 +8,7 @@ namespace Jellyfin.Api /// </summary> [ApiController] [Route("[controller]")] + [Produces(MediaTypeNames.Application.Json)] public class BaseJellyfinApiController : ControllerBase { } diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 8f23ef9d03..25d5d0c893 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{DFBEFB4C-DA19-4143-98B7-27320C7F7163}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> @@ -8,7 +13,7 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.3" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.4" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" /> </ItemGroup> diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs new file mode 100644 index 0000000000..522c206640 --- /dev/null +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -0,0 +1,154 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Data.Entities +{ + /// <summary> + /// An entity referencing an activity log entry. + /// </summary> + public partial class ActivityLog : ISavingChanges + { + /// <summary> + /// Initializes a new instance of the <see cref="ActivityLog"/> class. + /// Public constructor with required data. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="type">The type.</param> + /// <param name="userId">The user id.</param> + public ActivityLog(string name, string type, Guid userId) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentNullException(nameof(type)); + } + + this.Name = name; + this.Type = type; + this.UserId = userId; + this.DateCreated = DateTime.UtcNow; + this.LogSeverity = LogLevel.Trace; + + Init(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="ActivityLog"/> class. + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected ActivityLog() + { + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="name">The name.</param> + /// <param name="type">The type.</param> + /// <param name="userId">The user's id.</param> + /// <returns>The new <see cref="ActivityLog"/> instance.</returns> + public static ActivityLog Create(string name, string type, Guid userId) + { + return new ActivityLog(name, type, userId); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Gets or sets the identity of this instance. + /// This is the key in the backing database. + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name. + /// Required, Max length = 512. + /// </summary> + [Required] + [MaxLength(512)] + [StringLength(512)] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the overview. + /// Max length = 512. + /// </summary> + [MaxLength(512)] + [StringLength(512)] + public string Overview { get; set; } + + /// <summary> + /// Gets or sets the short overview. + /// Max length = 512. + /// </summary> + [MaxLength(512)] + [StringLength(512)] + public string ShortOverview { get; set; } + + /// <summary> + /// Gets or sets the type. + /// Required, Max length = 256. + /// </summary> + [Required] + [MaxLength(256)] + [StringLength(256)] + public string Type { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// Required. + /// </summary> + [Required] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the item id. + /// Max length = 256. + /// </summary> + [MaxLength(256)] + [StringLength(256)] + public string ItemId { get; set; } + + /// <summary> + /// Gets or sets the date created. This should be in UTC. + /// Required. + /// </summary> + [Required] + public DateTime DateCreated { get; set; } + + /// <summary> + /// Gets or sets the log severity. Default is <see cref="LogLevel.Trace"/>. + /// Required. + /// </summary> + [Required] + public LogLevel LogSeverity { get; set; } + + /// <summary> + /// Gets or sets the row version. + /// Required, ConcurrencyToken. + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + partial void Init(); + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Artwork.cs b/Jellyfin.Data/Entities/Artwork.cs new file mode 100644 index 0000000000..bf3029368a --- /dev/null +++ b/Jellyfin.Data/Entities/Artwork.cs @@ -0,0 +1,195 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities +{ + public partial class Artwork + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Artwork() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Artwork CreateArtworkUnsafe() + { + return new Artwork(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="path"></param> + /// <param name="kind"></param> + /// <param name="_metadata0"></param> + /// <param name="_personrole1"></param> + public Artwork(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1) + { + if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); + this.Path = path; + + this.Kind = kind; + + if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0)); + _metadata0.Artwork.Add(this); + + if (_personrole1 == null) throw new ArgumentNullException(nameof(_personrole1)); + _personrole1.Artwork = this; + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="path"></param> + /// <param name="kind"></param> + /// <param name="_metadata0"></param> + /// <param name="_personrole1"></param> + public static Artwork Create(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1) + { + return new Artwork(path, kind, _metadata0, _personrole1); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Path + /// </summary> + protected string _Path; + /// <summary> + /// When provided in a partial class, allows value of Path to be changed before setting. + /// </summary> + partial void SetPath(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Path to be changed before returning. + /// </summary> + partial void GetPath(ref string result); + + /// <summary> + /// Required, Max length = 65535 + /// </summary> + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string Path + { + get + { + string value = _Path; + GetPath(ref value); + return (_Path = value); + } + set + { + string oldValue = _Path; + SetPath(oldValue, ref value); + if (oldValue != value) + { + _Path = value; + } + } + } + + /// <summary> + /// Backing field for Kind + /// </summary> + internal Enums.ArtKind _Kind; + /// <summary> + /// When provided in a partial class, allows value of Kind to be changed before setting. + /// </summary> + partial void SetKind(Enums.ArtKind oldValue, ref Enums.ArtKind newValue); + /// <summary> + /// When provided in a partial class, allows value of Kind to be changed before returning. + /// </summary> + partial void GetKind(ref Enums.ArtKind result); + + /// <summary> + /// Indexed, Required + /// </summary> + [Required] + public Enums.ArtKind Kind + { + get + { + Enums.ArtKind value = _Kind; + GetKind(ref value); + return (_Kind = value); + } + set + { + Enums.ArtKind oldValue = _Kind; + SetKind(oldValue, ref value); + if (oldValue != value) + { + _Kind = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/Book.cs b/Jellyfin.Data/Entities/Book.cs new file mode 100644 index 0000000000..42d24e31d5 --- /dev/null +++ b/Jellyfin.Data/Entities/Book.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Book : LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Book() + { + BookMetadata = new HashSet<BookMetadata>(); + Releases = new HashSet<Release>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Book CreateBookUnsafe() + { + return new Book(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public Book(Guid urlid, DateTime dateadded) + { + this.UrlId = urlid; + + this.BookMetadata = new HashSet<BookMetadata>(); + this.Releases = new HashSet<Release>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public static Book Create(Guid urlid, DateTime dateadded) + { + return new Book(urlid, dateadded); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + [ForeignKey("BookMetadata_BookMetadata_Id")] + public virtual ICollection<BookMetadata> BookMetadata { get; protected set; } + + [ForeignKey("Release_Releases_Id")] + public virtual ICollection<Release> Releases { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/BookMetadata.cs b/Jellyfin.Data/Entities/BookMetadata.cs new file mode 100644 index 0000000000..d52fe76051 --- /dev/null +++ b/Jellyfin.Data/Entities/BookMetadata.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class BookMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected BookMetadata() + { + Publishers = new HashSet<Company>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static BookMetadata CreateBookMetadataUnsafe() + { + return new BookMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_book0"></param> + public BookMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_book0 == null) throw new ArgumentNullException(nameof(_book0)); + _book0.BookMetadata.Add(this); + + this.Publishers = new HashSet<Company>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_book0"></param> + public static BookMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0) + { + return new BookMetadata(title, language, dateadded, datemodified, _book0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for ISBN + /// </summary> + protected long? _ISBN; + /// <summary> + /// When provided in a partial class, allows value of ISBN to be changed before setting. + /// </summary> + partial void SetISBN(long? oldValue, ref long? newValue); + /// <summary> + /// When provided in a partial class, allows value of ISBN to be changed before returning. + /// </summary> + partial void GetISBN(ref long? result); + + public long? ISBN + { + get + { + long? value = _ISBN; + GetISBN(ref value); + return (_ISBN = value); + } + set + { + long? oldValue = _ISBN; + SetISBN(oldValue, ref value); + if (oldValue != value) + { + _ISBN = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + [ForeignKey("Company_Publishers_Id")] + public virtual ICollection<Company> Publishers { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs new file mode 100644 index 0000000000..d48cb9b627 --- /dev/null +++ b/Jellyfin.Data/Entities/Chapter.cs @@ -0,0 +1,263 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Chapter + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Chapter() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Chapter CreateChapterUnsafe() + { + return new Chapter(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="timestart"></param> + /// <param name="_release0"></param> + public Chapter(string language, long timestart, Release _release0) + { + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + this.TimeStart = timestart; + + if (_release0 == null) throw new ArgumentNullException(nameof(_release0)); + _release0.Chapters.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="timestart"></param> + /// <param name="_release0"></param> + public static Chapter Create(string language, long timestart, Release _release0) + { + return new Chapter(language, timestart, _release0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Name + /// </summary> + protected string _Name; + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before setting. + /// </summary> + partial void SetName(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before returning. + /// </summary> + partial void GetName(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Name + { + get + { + string value = _Name; + GetName(ref value); + return (_Name = value); + } + set + { + string oldValue = _Name; + SetName(oldValue, ref value); + if (oldValue != value) + { + _Name = value; + } + } + } + + /// <summary> + /// Backing field for Language + /// </summary> + protected string _Language; + /// <summary> + /// When provided in a partial class, allows value of Language to be changed before setting. + /// </summary> + partial void SetLanguage(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Language to be changed before returning. + /// </summary> + partial void GetLanguage(ref string result); + + /// <summary> + /// Required, Min length = 3, Max length = 3 + /// ISO-639-3 3-character language codes + /// </summary> + [Required] + [MinLength(3)] + [MaxLength(3)] + [StringLength(3)] + public string Language + { + get + { + string value = _Language; + GetLanguage(ref value); + return (_Language = value); + } + set + { + string oldValue = _Language; + SetLanguage(oldValue, ref value); + if (oldValue != value) + { + _Language = value; + } + } + } + + /// <summary> + /// Backing field for TimeStart + /// </summary> + protected long _TimeStart; + /// <summary> + /// When provided in a partial class, allows value of TimeStart to be changed before setting. + /// </summary> + partial void SetTimeStart(long oldValue, ref long newValue); + /// <summary> + /// When provided in a partial class, allows value of TimeStart to be changed before returning. + /// </summary> + partial void GetTimeStart(ref long result); + + /// <summary> + /// Required + /// </summary> + [Required] + public long TimeStart + { + get + { + long value = _TimeStart; + GetTimeStart(ref value); + return (_TimeStart = value); + } + set + { + long oldValue = _TimeStart; + SetTimeStart(oldValue, ref value); + if (oldValue != value) + { + _TimeStart = value; + } + } + } + + /// <summary> + /// Backing field for TimeEnd + /// </summary> + protected long? _TimeEnd; + /// <summary> + /// When provided in a partial class, allows value of TimeEnd to be changed before setting. + /// </summary> + partial void SetTimeEnd(long? oldValue, ref long? newValue); + /// <summary> + /// When provided in a partial class, allows value of TimeEnd to be changed before returning. + /// </summary> + partial void GetTimeEnd(ref long? result); + + public long? TimeEnd + { + get + { + long? value = _TimeEnd; + GetTimeEnd(ref value); + return (_TimeEnd = value); + } + set + { + long? oldValue = _TimeEnd; + SetTimeEnd(oldValue, ref value); + if (oldValue != value) + { + _TimeEnd = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/Collection.cs b/Jellyfin.Data/Entities/Collection.cs new file mode 100644 index 0000000000..e2fa3a5bd3 --- /dev/null +++ b/Jellyfin.Data/Entities/Collection.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Collection + { + partial void Init(); + + /// <summary> + /// Default constructor + /// </summary> + public Collection() + { + CollectionItem = new LinkedList<CollectionItem>(); + + Init(); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Name + /// </summary> + protected string _Name; + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before setting. + /// </summary> + partial void SetName(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before returning. + /// </summary> + partial void GetName(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Name + { + get + { + string value = _Name; + GetName(ref value); + return (_Name = value); + } + set + { + string oldValue = _Name; + SetName(oldValue, ref value); + if (oldValue != value) + { + _Name = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("CollectionItem_CollectionItem_Id")] + public virtual ICollection<CollectionItem> CollectionItem { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/CollectionItem.cs b/Jellyfin.Data/Entities/CollectionItem.cs new file mode 100644 index 0000000000..4a3d066396 --- /dev/null +++ b/Jellyfin.Data/Entities/CollectionItem.cs @@ -0,0 +1,143 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class CollectionItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected CollectionItem() + { + // NOTE: This class has one-to-one associations with CollectionItem. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static CollectionItem CreateCollectionItemUnsafe() + { + return new CollectionItem(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="_collection0"></param> + /// <param name="_collectionitem1"></param> + /// <param name="_collectionitem2"></param> + public CollectionItem(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2) + { + // NOTE: This class has one-to-one associations with CollectionItem. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + if (_collection0 == null) throw new ArgumentNullException(nameof(_collection0)); + _collection0.CollectionItem.Add(this); + + if (_collectionitem1 == null) throw new ArgumentNullException(nameof(_collectionitem1)); + _collectionitem1.Next = this; + + if (_collectionitem2 == null) throw new ArgumentNullException(nameof(_collectionitem2)); + _collectionitem2.Previous = this; + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="_collection0"></param> + /// <param name="_collectionitem1"></param> + /// <param name="_collectionitem2"></param> + public static CollectionItem Create(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2) + { + return new CollectionItem(_collection0, _collectionitem1, _collectionitem2); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + /// <summary> + /// Required + /// </summary> + [ForeignKey("LibraryItem_Id")] + public virtual LibraryItem LibraryItem { get; set; } + + /// <remarks> + /// TODO check if this properly updated dependant and has the proper principal relationship + /// </remarks> + [ForeignKey("CollectionItem_Next_Id")] + public virtual CollectionItem Next { get; set; } + + /// <remarks> + /// TODO check if this properly updated dependant and has the proper principal relationship + /// </remarks> + [ForeignKey("CollectionItem_Previous_Id")] + public virtual CollectionItem Previous { get; set; } + + } +} + diff --git a/Jellyfin.Data/Entities/Company.cs b/Jellyfin.Data/Entities/Company.cs new file mode 100644 index 0000000000..0650271c65 --- /dev/null +++ b/Jellyfin.Data/Entities/Company.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Company + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Company() + { + CompanyMetadata = new HashSet<CompanyMetadata>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Company CreateCompanyUnsafe() + { + return new Company(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="_moviemetadata0"></param> + /// <param name="_seriesmetadata1"></param> + /// <param name="_musicalbummetadata2"></param> + /// <param name="_bookmetadata3"></param> + /// <param name="_company4"></param> + public Company(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4) + { + if (_moviemetadata0 == null) throw new ArgumentNullException(nameof(_moviemetadata0)); + _moviemetadata0.Studios.Add(this); + + if (_seriesmetadata1 == null) throw new ArgumentNullException(nameof(_seriesmetadata1)); + _seriesmetadata1.Networks.Add(this); + + if (_musicalbummetadata2 == null) throw new ArgumentNullException(nameof(_musicalbummetadata2)); + _musicalbummetadata2.Labels.Add(this); + + if (_bookmetadata3 == null) throw new ArgumentNullException(nameof(_bookmetadata3)); + _bookmetadata3.Publishers.Add(this); + + if (_company4 == null) throw new ArgumentNullException(nameof(_company4)); + _company4.Parent = this; + + this.CompanyMetadata = new HashSet<CompanyMetadata>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="_moviemetadata0"></param> + /// <param name="_seriesmetadata1"></param> + /// <param name="_musicalbummetadata2"></param> + /// <param name="_bookmetadata3"></param> + /// <param name="_company4"></param> + public static Company Create(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4) + { + return new Company(_moviemetadata0, _seriesmetadata1, _musicalbummetadata2, _bookmetadata3, _company4); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("CompanyMetadata_CompanyMetadata_Id")] + public virtual ICollection<CompanyMetadata> CompanyMetadata { get; protected set; } + [ForeignKey("Company_Parent_Id")] + public virtual Company Parent { get; set; } + + } +} + diff --git a/Jellyfin.Data/Entities/CompanyMetadata.cs b/Jellyfin.Data/Entities/CompanyMetadata.cs new file mode 100644 index 0000000000..b3ec9c1a7f --- /dev/null +++ b/Jellyfin.Data/Entities/CompanyMetadata.cs @@ -0,0 +1,216 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities +{ + public partial class CompanyMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected CompanyMetadata() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static CompanyMetadata CreateCompanyMetadataUnsafe() + { + return new CompanyMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_company0"></param> + public CompanyMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_company0 == null) throw new ArgumentNullException(nameof(_company0)); + _company0.CompanyMetadata.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_company0"></param> + public static CompanyMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0) + { + return new CompanyMetadata(title, language, dateadded, datemodified, _company0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Description + /// </summary> + protected string _Description; + /// <summary> + /// When provided in a partial class, allows value of Description to be changed before setting. + /// </summary> + partial void SetDescription(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Description to be changed before returning. + /// </summary> + partial void GetDescription(ref string result); + + /// <summary> + /// Max length = 65535 + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string Description + { + get + { + string value = _Description; + GetDescription(ref value); + return (_Description = value); + } + set + { + string oldValue = _Description; + SetDescription(oldValue, ref value); + if (oldValue != value) + { + _Description = value; + } + } + } + + /// <summary> + /// Backing field for Headquarters + /// </summary> + protected string _Headquarters; + /// <summary> + /// When provided in a partial class, allows value of Headquarters to be changed before setting. + /// </summary> + partial void SetHeadquarters(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Headquarters to be changed before returning. + /// </summary> + partial void GetHeadquarters(ref string result); + + /// <summary> + /// Max length = 255 + /// </summary> + [MaxLength(255)] + [StringLength(255)] + public string Headquarters + { + get + { + string value = _Headquarters; + GetHeadquarters(ref value); + return (_Headquarters = value); + } + set + { + string oldValue = _Headquarters; + SetHeadquarters(oldValue, ref value); + if (oldValue != value) + { + _Headquarters = value; + } + } + } + + /// <summary> + /// Backing field for Country + /// </summary> + protected string _Country; + /// <summary> + /// When provided in a partial class, allows value of Country to be changed before setting. + /// </summary> + partial void SetCountry(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Country to be changed before returning. + /// </summary> + partial void GetCountry(ref string result); + + /// <summary> + /// Max length = 2 + /// </summary> + [MaxLength(2)] + [StringLength(2)] + public string Country + { + get + { + string value = _Country; + GetCountry(ref value); + return (_Country = value); + } + set + { + string oldValue = _Country; + SetCountry(oldValue, ref value); + if (oldValue != value) + { + _Country = value; + } + } + } + + /// <summary> + /// Backing field for Homepage + /// </summary> + protected string _Homepage; + /// <summary> + /// When provided in a partial class, allows value of Homepage to be changed before setting. + /// </summary> + partial void SetHomepage(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Homepage to be changed before returning. + /// </summary> + partial void GetHomepage(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Homepage + { + get + { + string value = _Homepage; + GetHomepage(ref value); + return (_Homepage = value); + } + set + { + string oldValue = _Homepage; + SetHomepage(oldValue, ref value); + if (oldValue != value) + { + _Homepage = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/CustomItem.cs b/Jellyfin.Data/Entities/CustomItem.cs new file mode 100644 index 0000000000..2006717bf2 --- /dev/null +++ b/Jellyfin.Data/Entities/CustomItem.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class CustomItem : LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected CustomItem() + { + CustomItemMetadata = new HashSet<CustomItemMetadata>(); + Releases = new HashSet<Release>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static CustomItem CreateCustomItemUnsafe() + { + return new CustomItem(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public CustomItem(Guid urlid, DateTime dateadded) + { + this.UrlId = urlid; + + this.CustomItemMetadata = new HashSet<CustomItemMetadata>(); + this.Releases = new HashSet<Release>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public static CustomItem Create(Guid urlid, DateTime dateadded) + { + return new CustomItem(urlid, dateadded); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("CustomItemMetadata_CustomItemMetadata_Id")] + public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; protected set; } + + [ForeignKey("Release_Releases_Id")] + public virtual ICollection<Release> Releases { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/CustomItemMetadata.cs b/Jellyfin.Data/Entities/CustomItemMetadata.cs new file mode 100644 index 0000000000..e09e4467ac --- /dev/null +++ b/Jellyfin.Data/Entities/CustomItemMetadata.cs @@ -0,0 +1,67 @@ +using System; + +namespace Jellyfin.Data.Entities +{ + public partial class CustomItemMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected CustomItemMetadata() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static CustomItemMetadata CreateCustomItemMetadataUnsafe() + { + return new CustomItemMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_customitem0"></param> + public CustomItemMetadata(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_customitem0 == null) throw new ArgumentNullException(nameof(_customitem0)); + _customitem0.CustomItemMetadata.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_customitem0"></param> + public static CustomItemMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0) + { + return new CustomItemMetadata(title, language, dateadded, datemodified, _customitem0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/Episode.cs b/Jellyfin.Data/Entities/Episode.cs new file mode 100644 index 0000000000..6f6baa14de --- /dev/null +++ b/Jellyfin.Data/Entities/Episode.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Episode : LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Episode() + { + // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + Releases = new HashSet<Release>(); + EpisodeMetadata = new HashSet<EpisodeMetadata>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Episode CreateEpisodeUnsafe() + { + return new Episode(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + /// <param name="_season0"></param> + public Episode(Guid urlid, DateTime dateadded, Season _season0) + { + // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + this.UrlId = urlid; + + if (_season0 == null) throw new ArgumentNullException(nameof(_season0)); + _season0.Episodes.Add(this); + + this.Releases = new HashSet<Release>(); + this.EpisodeMetadata = new HashSet<EpisodeMetadata>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + /// <param name="_season0"></param> + public static Episode Create(Guid urlid, DateTime dateadded, Season _season0) + { + return new Episode(urlid, dateadded, _season0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for EpisodeNumber + /// </summary> + protected int? _EpisodeNumber; + /// <summary> + /// When provided in a partial class, allows value of EpisodeNumber to be changed before setting. + /// </summary> + partial void SetEpisodeNumber(int? oldValue, ref int? newValue); + /// <summary> + /// When provided in a partial class, allows value of EpisodeNumber to be changed before returning. + /// </summary> + partial void GetEpisodeNumber(ref int? result); + + public int? EpisodeNumber + { + get + { + int? value = _EpisodeNumber; + GetEpisodeNumber(ref value); + return (_EpisodeNumber = value); + } + set + { + int? oldValue = _EpisodeNumber; + SetEpisodeNumber(oldValue, ref value); + if (oldValue != value) + { + _EpisodeNumber = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("Release_Releases_Id")] + public virtual ICollection<Release> Releases { get; protected set; } + [ForeignKey("EpisodeMetadata_EpisodeMetadata_Id")] + public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/EpisodeMetadata.cs b/Jellyfin.Data/Entities/EpisodeMetadata.cs new file mode 100644 index 0000000000..e5431bf223 --- /dev/null +++ b/Jellyfin.Data/Entities/EpisodeMetadata.cs @@ -0,0 +1,179 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities +{ + public partial class EpisodeMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected EpisodeMetadata() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static EpisodeMetadata CreateEpisodeMetadataUnsafe() + { + return new EpisodeMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_episode0"></param> + public EpisodeMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_episode0 == null) throw new ArgumentNullException(nameof(_episode0)); + _episode0.EpisodeMetadata.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_episode0"></param> + public static EpisodeMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0) + { + return new EpisodeMetadata(title, language, dateadded, datemodified, _episode0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Outline + /// </summary> + protected string _Outline; + /// <summary> + /// When provided in a partial class, allows value of Outline to be changed before setting. + /// </summary> + partial void SetOutline(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Outline to be changed before returning. + /// </summary> + partial void GetOutline(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Outline + { + get + { + string value = _Outline; + GetOutline(ref value); + return (_Outline = value); + } + set + { + string oldValue = _Outline; + SetOutline(oldValue, ref value); + if (oldValue != value) + { + _Outline = value; + } + } + } + + /// <summary> + /// Backing field for Plot + /// </summary> + protected string _Plot; + /// <summary> + /// When provided in a partial class, allows value of Plot to be changed before setting. + /// </summary> + partial void SetPlot(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Plot to be changed before returning. + /// </summary> + partial void GetPlot(ref string result); + + /// <summary> + /// Max length = 65535 + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string Plot + { + get + { + string value = _Plot; + GetPlot(ref value); + return (_Plot = value); + } + set + { + string oldValue = _Plot; + SetPlot(oldValue, ref value); + if (oldValue != value) + { + _Plot = value; + } + } + } + + /// <summary> + /// Backing field for Tagline + /// </summary> + protected string _Tagline; + /// <summary> + /// When provided in a partial class, allows value of Tagline to be changed before setting. + /// </summary> + partial void SetTagline(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Tagline to be changed before returning. + /// </summary> + partial void GetTagline(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Tagline + { + get + { + string value = _Tagline; + GetTagline(ref value); + return (_Tagline = value); + } + set + { + string oldValue = _Tagline; + SetTagline(oldValue, ref value); + if (oldValue != value) + { + _Tagline = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/Genre.cs b/Jellyfin.Data/Entities/Genre.cs new file mode 100644 index 0000000000..38f289a8e3 --- /dev/null +++ b/Jellyfin.Data/Entities/Genre.cs @@ -0,0 +1,152 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Genre + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Genre() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Genre CreateGenreUnsafe() + { + return new Genre(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="name"></param> + /// <param name="_metadata0"></param> + public Genre(string name, Metadata _metadata0) + { + if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); + this.Name = name; + + if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0)); + _metadata0.Genres.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="name"></param> + /// <param name="_metadata0"></param> + public static Genre Create(string name, Metadata _metadata0) + { + return new Genre(name, _metadata0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Name + /// </summary> + internal string _Name; + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before setting. + /// </summary> + partial void SetName(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before returning. + /// </summary> + partial void GetName(ref string result); + + /// <summary> + /// Indexed, Required, Max length = 255 + /// </summary> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string Name + { + get + { + string value = _Name; + GetName(ref value); + return (_Name = value); + } + set + { + string oldValue = _Name; + SetName(oldValue, ref value); + if (oldValue != value) + { + _Name = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs new file mode 100644 index 0000000000..54f9f49057 --- /dev/null +++ b/Jellyfin.Data/Entities/Group.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Group + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Group() + { + GroupPermissions = new HashSet<Permission>(); + ProviderMappings = new HashSet<ProviderMapping>(); + Preferences = new HashSet<Preference>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Group CreateGroupUnsafe() + { + return new Group(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="name"></param> + /// <param name="_user0"></param> + public Group(string name, User _user0) + { + if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); + this.Name = name; + + if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); + _user0.Groups.Add(this); + + this.GroupPermissions = new HashSet<Permission>(); + this.ProviderMappings = new HashSet<ProviderMapping>(); + this.Preferences = new HashSet<Preference>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="name"></param> + /// <param name="_user0"></param> + public static Group Create(string name, User _user0) + { + return new Group(name, _user0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Required, Max length = 255 + /// </summary> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string Name { get; set; } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + [ForeignKey("Permission_GroupPermissions_Id")] + public virtual ICollection<Permission> GroupPermissions { get; protected set; } + + [ForeignKey("ProviderMapping_ProviderMappings_Id")] + public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; } + + [ForeignKey("Preference_Preferences_Id")] + public virtual ICollection<Preference> Preferences { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/Library.cs b/Jellyfin.Data/Entities/Library.cs new file mode 100644 index 0000000000..c11c09e916 --- /dev/null +++ b/Jellyfin.Data/Entities/Library.cs @@ -0,0 +1,147 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Library + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Library() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Library CreateLibraryUnsafe() + { + return new Library(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="name"></param> + public Library(string name) + { + if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); + this.Name = name; + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="name"></param> + public static Library Create(string name) + { + return new Library(name); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Name + /// </summary> + protected string _Name; + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before setting. + /// </summary> + partial void SetName(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before returning. + /// </summary> + partial void GetName(ref string result); + + /// <summary> + /// Required, Max length = 1024 + /// </summary> + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name + { + get + { + string value = _Name; + GetName(ref value); + return (_Name = value); + } + set + { + string oldValue = _Name; + SetName(oldValue, ref value); + if (oldValue != value) + { + _Name = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/LibraryItem.cs b/Jellyfin.Data/Entities/LibraryItem.cs new file mode 100644 index 0000000000..af6c640b97 --- /dev/null +++ b/Jellyfin.Data/Entities/LibraryItem.cs @@ -0,0 +1,170 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public abstract partial class LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to being abstract. + /// </summary> + protected LibraryItem() + { + Init(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + protected LibraryItem(Guid urlid, DateTime dateadded) + { + this.UrlId = urlid; + + + Init(); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for UrlId + /// </summary> + internal Guid _UrlId; + /// <summary> + /// When provided in a partial class, allows value of UrlId to be changed before setting. + /// </summary> + partial void SetUrlId(Guid oldValue, ref Guid newValue); + /// <summary> + /// When provided in a partial class, allows value of UrlId to be changed before returning. + /// </summary> + partial void GetUrlId(ref Guid result); + + /// <summary> + /// Indexed, Required + /// This is whats gets displayed in the Urls and API requests. This could also be a string. + /// </summary> + [Required] + public Guid UrlId + { + get + { + Guid value = _UrlId; + GetUrlId(ref value); + return (_UrlId = value); + } + set + { + Guid oldValue = _UrlId; + SetUrlId(oldValue, ref value); + if (oldValue != value) + { + _UrlId = value; + } + } + } + + /// <summary> + /// Backing field for DateAdded + /// </summary> + protected DateTime _DateAdded; + /// <summary> + /// When provided in a partial class, allows value of DateAdded to be changed before setting. + /// </summary> + partial void SetDateAdded(DateTime oldValue, ref DateTime newValue); + /// <summary> + /// When provided in a partial class, allows value of DateAdded to be changed before returning. + /// </summary> + partial void GetDateAdded(ref DateTime result); + + /// <summary> + /// Required + /// </summary> + [Required] + public DateTime DateAdded + { + get + { + DateTime value = _DateAdded; + GetDateAdded(ref value); + return (_DateAdded = value); + } + internal set + { + DateTime oldValue = _DateAdded; + SetDateAdded(oldValue, ref value); + if (oldValue != value) + { + _DateAdded = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + /// <summary> + /// Required + /// </summary> + [ForeignKey("LibraryRoot_Id")] + public virtual LibraryRoot LibraryRoot { get; set; } + + } +} + diff --git a/Jellyfin.Data/Entities/LibraryRoot.cs b/Jellyfin.Data/Entities/LibraryRoot.cs new file mode 100644 index 0000000000..bbc23e1c96 --- /dev/null +++ b/Jellyfin.Data/Entities/LibraryRoot.cs @@ -0,0 +1,192 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class LibraryRoot + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected LibraryRoot() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static LibraryRoot CreateLibraryRootUnsafe() + { + return new LibraryRoot(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="path">Absolute Path</param> + public LibraryRoot(string path) + { + if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); + this.Path = path; + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="path">Absolute Path</param> + public static LibraryRoot Create(string path) + { + return new LibraryRoot(path); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Path + /// </summary> + protected string _Path; + /// <summary> + /// When provided in a partial class, allows value of Path to be changed before setting. + /// </summary> + partial void SetPath(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Path to be changed before returning. + /// </summary> + partial void GetPath(ref string result); + + /// <summary> + /// Required, Max length = 65535 + /// Absolute Path + /// </summary> + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string Path + { + get + { + string value = _Path; + GetPath(ref value); + return (_Path = value); + } + set + { + string oldValue = _Path; + SetPath(oldValue, ref value); + if (oldValue != value) + { + _Path = value; + } + } + } + + /// <summary> + /// Backing field for NetworkPath + /// </summary> + protected string _NetworkPath; + /// <summary> + /// When provided in a partial class, allows value of NetworkPath to be changed before setting. + /// </summary> + partial void SetNetworkPath(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of NetworkPath to be changed before returning. + /// </summary> + partial void GetNetworkPath(ref string result); + + /// <summary> + /// Max length = 65535 + /// Absolute network path, for example for transcoding sattelites. + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string NetworkPath + { + get + { + string value = _NetworkPath; + GetNetworkPath(ref value); + return (_NetworkPath = value); + } + set + { + string oldValue = _NetworkPath; + SetNetworkPath(oldValue, ref value); + if (oldValue != value) + { + _NetworkPath = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + /// <summary> + /// Required + /// </summary> + [ForeignKey("Library_Id")] + public virtual Library Library { get; set; } + + } +} + diff --git a/Jellyfin.Data/Entities/MediaFile.cs b/Jellyfin.Data/Entities/MediaFile.cs new file mode 100644 index 0000000000..719539e5c2 --- /dev/null +++ b/Jellyfin.Data/Entities/MediaFile.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class MediaFile + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected MediaFile() + { + MediaFileStreams = new HashSet<MediaFileStream>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static MediaFile CreateMediaFileUnsafe() + { + return new MediaFile(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="path">Relative to the LibraryRoot</param> + /// <param name="kind"></param> + /// <param name="_release0"></param> + public MediaFile(string path, Enums.MediaFileKind kind, Release _release0) + { + if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); + this.Path = path; + + this.Kind = kind; + + if (_release0 == null) throw new ArgumentNullException(nameof(_release0)); + _release0.MediaFiles.Add(this); + + this.MediaFileStreams = new HashSet<MediaFileStream>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="path">Relative to the LibraryRoot</param> + /// <param name="kind"></param> + /// <param name="_release0"></param> + public static MediaFile Create(string path, Enums.MediaFileKind kind, Release _release0) + { + return new MediaFile(path, kind, _release0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Path + /// </summary> + protected string _Path; + /// <summary> + /// When provided in a partial class, allows value of Path to be changed before setting. + /// </summary> + partial void SetPath(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Path to be changed before returning. + /// </summary> + partial void GetPath(ref string result); + + /// <summary> + /// Required, Max length = 65535 + /// Relative to the LibraryRoot + /// </summary> + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string Path + { + get + { + string value = _Path; + GetPath(ref value); + return (_Path = value); + } + set + { + string oldValue = _Path; + SetPath(oldValue, ref value); + if (oldValue != value) + { + _Path = value; + } + } + } + + /// <summary> + /// Backing field for Kind + /// </summary> + protected Enums.MediaFileKind _Kind; + /// <summary> + /// When provided in a partial class, allows value of Kind to be changed before setting. + /// </summary> + partial void SetKind(Enums.MediaFileKind oldValue, ref Enums.MediaFileKind newValue); + /// <summary> + /// When provided in a partial class, allows value of Kind to be changed before returning. + /// </summary> + partial void GetKind(ref Enums.MediaFileKind result); + + /// <summary> + /// Required + /// </summary> + [Required] + public Enums.MediaFileKind Kind + { + get + { + Enums.MediaFileKind value = _Kind; + GetKind(ref value); + return (_Kind = value); + } + set + { + Enums.MediaFileKind oldValue = _Kind; + SetKind(oldValue, ref value); + if (oldValue != value) + { + _Kind = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + [ForeignKey("MediaFileStream_MediaFileStreams_Id")] + public virtual ICollection<MediaFileStream> MediaFileStreams { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/MediaFileStream.cs b/Jellyfin.Data/Entities/MediaFileStream.cs new file mode 100644 index 0000000000..7b3399731a --- /dev/null +++ b/Jellyfin.Data/Entities/MediaFileStream.cs @@ -0,0 +1,149 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class MediaFileStream + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected MediaFileStream() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static MediaFileStream CreateMediaFileStreamUnsafe() + { + return new MediaFileStream(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="streamnumber"></param> + /// <param name="_mediafile0"></param> + public MediaFileStream(int streamnumber, MediaFile _mediafile0) + { + this.StreamNumber = streamnumber; + + if (_mediafile0 == null) throw new ArgumentNullException(nameof(_mediafile0)); + _mediafile0.MediaFileStreams.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="streamnumber"></param> + /// <param name="_mediafile0"></param> + public static MediaFileStream Create(int streamnumber, MediaFile _mediafile0) + { + return new MediaFileStream(streamnumber, _mediafile0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for StreamNumber + /// </summary> + protected int _StreamNumber; + /// <summary> + /// When provided in a partial class, allows value of StreamNumber to be changed before setting. + /// </summary> + partial void SetStreamNumber(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of StreamNumber to be changed before returning. + /// </summary> + partial void GetStreamNumber(ref int result); + + /// <summary> + /// Required + /// </summary> + [Required] + public int StreamNumber + { + get + { + int value = _StreamNumber; + GetStreamNumber(ref value); + return (_StreamNumber = value); + } + set + { + int oldValue = _StreamNumber; + SetStreamNumber(oldValue, ref value); + if (oldValue != value) + { + _StreamNumber = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/Metadata.cs b/Jellyfin.Data/Entities/Metadata.cs new file mode 100644 index 0000000000..467ee68226 --- /dev/null +++ b/Jellyfin.Data/Entities/Metadata.cs @@ -0,0 +1,380 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public abstract partial class Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to being abstract. + /// </summary> + protected Metadata() + { + PersonRoles = new HashSet<PersonRole>(); + Genres = new HashSet<Genre>(); + Artwork = new HashSet<Artwork>(); + Ratings = new HashSet<Rating>(); + Sources = new HashSet<MetadataProviderId>(); + + Init(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + protected Metadata(string title, string language, DateTime dateadded, DateTime datemodified) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + this.PersonRoles = new HashSet<PersonRole>(); + this.Genres = new HashSet<Genre>(); + this.Artwork = new HashSet<Artwork>(); + this.Ratings = new HashSet<Rating>(); + this.Sources = new HashSet<MetadataProviderId>(); + + Init(); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Title + /// </summary> + protected string _Title; + /// <summary> + /// When provided in a partial class, allows value of Title to be changed before setting. + /// </summary> + partial void SetTitle(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Title to be changed before returning. + /// </summary> + partial void GetTitle(ref string result); + + /// <summary> + /// Required, Max length = 1024 + /// The title or name of the object + /// </summary> + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Title + { + get + { + string value = _Title; + GetTitle(ref value); + return (_Title = value); + } + set + { + string oldValue = _Title; + SetTitle(oldValue, ref value); + if (oldValue != value) + { + _Title = value; + } + } + } + + /// <summary> + /// Backing field for OriginalTitle + /// </summary> + protected string _OriginalTitle; + /// <summary> + /// When provided in a partial class, allows value of OriginalTitle to be changed before setting. + /// </summary> + partial void SetOriginalTitle(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of OriginalTitle to be changed before returning. + /// </summary> + partial void GetOriginalTitle(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string OriginalTitle + { + get + { + string value = _OriginalTitle; + GetOriginalTitle(ref value); + return (_OriginalTitle = value); + } + set + { + string oldValue = _OriginalTitle; + SetOriginalTitle(oldValue, ref value); + if (oldValue != value) + { + _OriginalTitle = value; + } + } + } + + /// <summary> + /// Backing field for SortTitle + /// </summary> + protected string _SortTitle; + /// <summary> + /// When provided in a partial class, allows value of SortTitle to be changed before setting. + /// </summary> + partial void SetSortTitle(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of SortTitle to be changed before returning. + /// </summary> + partial void GetSortTitle(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string SortTitle + { + get + { + string value = _SortTitle; + GetSortTitle(ref value); + return (_SortTitle = value); + } + set + { + string oldValue = _SortTitle; + SetSortTitle(oldValue, ref value); + if (oldValue != value) + { + _SortTitle = value; + } + } + } + + /// <summary> + /// Backing field for Language + /// </summary> + protected string _Language; + /// <summary> + /// When provided in a partial class, allows value of Language to be changed before setting. + /// </summary> + partial void SetLanguage(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Language to be changed before returning. + /// </summary> + partial void GetLanguage(ref string result); + + /// <summary> + /// Required, Min length = 3, Max length = 3 + /// ISO-639-3 3-character language codes + /// </summary> + [Required] + [MinLength(3)] + [MaxLength(3)] + [StringLength(3)] + public string Language + { + get + { + string value = _Language; + GetLanguage(ref value); + return (_Language = value); + } + set + { + string oldValue = _Language; + SetLanguage(oldValue, ref value); + if (oldValue != value) + { + _Language = value; + } + } + } + + /// <summary> + /// Backing field for ReleaseDate + /// </summary> + protected DateTimeOffset? _ReleaseDate; + /// <summary> + /// When provided in a partial class, allows value of ReleaseDate to be changed before setting. + /// </summary> + partial void SetReleaseDate(DateTimeOffset? oldValue, ref DateTimeOffset? newValue); + /// <summary> + /// When provided in a partial class, allows value of ReleaseDate to be changed before returning. + /// </summary> + partial void GetReleaseDate(ref DateTimeOffset? result); + + public DateTimeOffset? ReleaseDate + { + get + { + DateTimeOffset? value = _ReleaseDate; + GetReleaseDate(ref value); + return (_ReleaseDate = value); + } + set + { + DateTimeOffset? oldValue = _ReleaseDate; + SetReleaseDate(oldValue, ref value); + if (oldValue != value) + { + _ReleaseDate = value; + } + } + } + + /// <summary> + /// Backing field for DateAdded + /// </summary> + protected DateTime _DateAdded; + /// <summary> + /// When provided in a partial class, allows value of DateAdded to be changed before setting. + /// </summary> + partial void SetDateAdded(DateTime oldValue, ref DateTime newValue); + /// <summary> + /// When provided in a partial class, allows value of DateAdded to be changed before returning. + /// </summary> + partial void GetDateAdded(ref DateTime result); + + /// <summary> + /// Required + /// </summary> + [Required] + public DateTime DateAdded + { + get + { + DateTime value = _DateAdded; + GetDateAdded(ref value); + return (_DateAdded = value); + } + internal set + { + DateTime oldValue = _DateAdded; + SetDateAdded(oldValue, ref value); + if (oldValue != value) + { + _DateAdded = value; + } + } + } + + /// <summary> + /// Backing field for DateModified + /// </summary> + protected DateTime _DateModified; + /// <summary> + /// When provided in a partial class, allows value of DateModified to be changed before setting. + /// </summary> + partial void SetDateModified(DateTime oldValue, ref DateTime newValue); + /// <summary> + /// When provided in a partial class, allows value of DateModified to be changed before returning. + /// </summary> + partial void GetDateModified(ref DateTime result); + + /// <summary> + /// Required + /// </summary> + [Required] + public DateTime DateModified + { + get + { + DateTime value = _DateModified; + GetDateModified(ref value); + return (_DateModified = value); + } + internal set + { + DateTime oldValue = _DateModified; + SetDateModified(oldValue, ref value); + if (oldValue != value) + { + _DateModified = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + [ForeignKey("PersonRole_PersonRoles_Id")] + public virtual ICollection<PersonRole> PersonRoles { get; protected set; } + + [ForeignKey("PersonRole_PersonRoles_Id")] + public virtual ICollection<Genre> Genres { get; protected set; } + + [ForeignKey("PersonRole_PersonRoles_Id")] + public virtual ICollection<Artwork> Artwork { get; protected set; } + + [ForeignKey("PersonRole_PersonRoles_Id")] + public virtual ICollection<Rating> Ratings { get; protected set; } + + [ForeignKey("PersonRole_PersonRoles_Id")] + public virtual ICollection<MetadataProviderId> Sources { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/MetadataProvider.cs b/Jellyfin.Data/Entities/MetadataProvider.cs new file mode 100644 index 0000000000..4e4f107fb9 --- /dev/null +++ b/Jellyfin.Data/Entities/MetadataProvider.cs @@ -0,0 +1,147 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class MetadataProvider + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected MetadataProvider() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static MetadataProvider CreateMetadataProviderUnsafe() + { + return new MetadataProvider(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="name"></param> + public MetadataProvider(string name) + { + if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); + this.Name = name; + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="name"></param> + public static MetadataProvider Create(string name) + { + return new MetadataProvider(name); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Name + /// </summary> + protected string _Name; + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before setting. + /// </summary> + partial void SetName(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before returning. + /// </summary> + partial void GetName(ref string result); + + /// <summary> + /// Required, Max length = 1024 + /// </summary> + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name + { + get + { + string value = _Name; + GetName(ref value); + return (_Name = value); + } + set + { + string oldValue = _Name; + SetName(oldValue, ref value); + if (oldValue != value) + { + _Name = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/MetadataProviderId.cs b/Jellyfin.Data/Entities/MetadataProviderId.cs new file mode 100644 index 0000000000..926f223dea --- /dev/null +++ b/Jellyfin.Data/Entities/MetadataProviderId.cs @@ -0,0 +1,179 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class MetadataProviderId + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected MetadataProviderId() + { + // NOTE: This class has one-to-one associations with MetadataProviderId. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static MetadataProviderId CreateMetadataProviderIdUnsafe() + { + return new MetadataProviderId(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="providerid"></param> + /// <param name="_metadata0"></param> + /// <param name="_person1"></param> + /// <param name="_personrole2"></param> + /// <param name="_ratingsource3"></param> + public MetadataProviderId(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3) + { + // NOTE: This class has one-to-one associations with MetadataProviderId. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + if (string.IsNullOrEmpty(providerid)) throw new ArgumentNullException(nameof(providerid)); + this.ProviderId = providerid; + + if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0)); + _metadata0.Sources.Add(this); + + if (_person1 == null) throw new ArgumentNullException(nameof(_person1)); + _person1.Sources.Add(this); + + if (_personrole2 == null) throw new ArgumentNullException(nameof(_personrole2)); + _personrole2.Sources.Add(this); + + if (_ratingsource3 == null) throw new ArgumentNullException(nameof(_ratingsource3)); + _ratingsource3.Source = this; + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="providerid"></param> + /// <param name="_metadata0"></param> + /// <param name="_person1"></param> + /// <param name="_personrole2"></param> + /// <param name="_ratingsource3"></param> + public static MetadataProviderId Create(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3) + { + return new MetadataProviderId(providerid, _metadata0, _person1, _personrole2, _ratingsource3); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for ProviderId + /// </summary> + protected string _ProviderId; + /// <summary> + /// When provided in a partial class, allows value of ProviderId to be changed before setting. + /// </summary> + partial void SetProviderId(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of ProviderId to be changed before returning. + /// </summary> + partial void GetProviderId(ref string result); + + /// <summary> + /// Required, Max length = 255 + /// </summary> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string ProviderId + { + get + { + string value = _ProviderId; + GetProviderId(ref value); + return (_ProviderId = value); + } + set + { + string oldValue = _ProviderId; + SetProviderId(oldValue, ref value); + if (oldValue != value) + { + _ProviderId = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + /// <summary> + /// Required + /// </summary> + [ForeignKey("MetadataProvider_Id")] + public virtual MetadataProvider MetadataProvider { get; set; } + + } +} + diff --git a/Jellyfin.Data/Entities/Movie.cs b/Jellyfin.Data/Entities/Movie.cs new file mode 100644 index 0000000000..b359b42fcd --- /dev/null +++ b/Jellyfin.Data/Entities/Movie.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Movie : LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Movie() + { + Releases = new HashSet<Release>(); + MovieMetadata = new HashSet<MovieMetadata>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Movie CreateMovieUnsafe() + { + return new Movie(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public Movie(Guid urlid, DateTime dateadded) + { + this.UrlId = urlid; + + this.Releases = new HashSet<Release>(); + this.MovieMetadata = new HashSet<MovieMetadata>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public static Movie Create(Guid urlid, DateTime dateadded) + { + return new Movie(urlid, dateadded); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + [ForeignKey("Release_Releases_Id")] + public virtual ICollection<Release> Releases { get; protected set; } + + [ForeignKey("MovieMetadata_MovieMetadata_Id")] + public virtual ICollection<MovieMetadata> MovieMetadata { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/MovieMetadata.cs b/Jellyfin.Data/Entities/MovieMetadata.cs new file mode 100644 index 0000000000..319ae94e5a --- /dev/null +++ b/Jellyfin.Data/Entities/MovieMetadata.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class MovieMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected MovieMetadata() + { + Studios = new HashSet<Company>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static MovieMetadata CreateMovieMetadataUnsafe() + { + return new MovieMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_movie0"></param> + public MovieMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_movie0 == null) throw new ArgumentNullException(nameof(_movie0)); + _movie0.MovieMetadata.Add(this); + + this.Studios = new HashSet<Company>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_movie0"></param> + public static MovieMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0) + { + return new MovieMetadata(title, language, dateadded, datemodified, _movie0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Outline + /// </summary> + protected string _Outline; + /// <summary> + /// When provided in a partial class, allows value of Outline to be changed before setting. + /// </summary> + partial void SetOutline(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Outline to be changed before returning. + /// </summary> + partial void GetOutline(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Outline + { + get + { + string value = _Outline; + GetOutline(ref value); + return (_Outline = value); + } + set + { + string oldValue = _Outline; + SetOutline(oldValue, ref value); + if (oldValue != value) + { + _Outline = value; + } + } + } + + /// <summary> + /// Backing field for Plot + /// </summary> + protected string _Plot; + /// <summary> + /// When provided in a partial class, allows value of Plot to be changed before setting. + /// </summary> + partial void SetPlot(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Plot to be changed before returning. + /// </summary> + partial void GetPlot(ref string result); + + /// <summary> + /// Max length = 65535 + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string Plot + { + get + { + string value = _Plot; + GetPlot(ref value); + return (_Plot = value); + } + set + { + string oldValue = _Plot; + SetPlot(oldValue, ref value); + if (oldValue != value) + { + _Plot = value; + } + } + } + + /// <summary> + /// Backing field for Tagline + /// </summary> + protected string _Tagline; + /// <summary> + /// When provided in a partial class, allows value of Tagline to be changed before setting. + /// </summary> + partial void SetTagline(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Tagline to be changed before returning. + /// </summary> + partial void GetTagline(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Tagline + { + get + { + string value = _Tagline; + GetTagline(ref value); + return (_Tagline = value); + } + set + { + string oldValue = _Tagline; + SetTagline(oldValue, ref value); + if (oldValue != value) + { + _Tagline = value; + } + } + } + + /// <summary> + /// Backing field for Country + /// </summary> + protected string _Country; + /// <summary> + /// When provided in a partial class, allows value of Country to be changed before setting. + /// </summary> + partial void SetCountry(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Country to be changed before returning. + /// </summary> + partial void GetCountry(ref string result); + + /// <summary> + /// Max length = 2 + /// </summary> + [MaxLength(2)] + [StringLength(2)] + public string Country + { + get + { + string value = _Country; + GetCountry(ref value); + return (_Country = value); + } + set + { + string oldValue = _Country; + SetCountry(oldValue, ref value); + if (oldValue != value) + { + _Country = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("Company_Studios_Id")] + public virtual ICollection<Company> Studios { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/MusicAlbum.cs b/Jellyfin.Data/Entities/MusicAlbum.cs new file mode 100644 index 0000000000..00cb8fe007 --- /dev/null +++ b/Jellyfin.Data/Entities/MusicAlbum.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class MusicAlbum : LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected MusicAlbum() + { + MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>(); + Tracks = new HashSet<Track>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static MusicAlbum CreateMusicAlbumUnsafe() + { + return new MusicAlbum(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public MusicAlbum(Guid urlid, DateTime dateadded) + { + this.UrlId = urlid; + + this.MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>(); + this.Tracks = new HashSet<Track>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public static MusicAlbum Create(Guid urlid, DateTime dateadded) + { + return new MusicAlbum(urlid, dateadded); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("MusicAlbumMetadata_MusicAlbumMetadata_Id")] + public virtual ICollection<MusicAlbumMetadata> MusicAlbumMetadata { get; protected set; } + + [ForeignKey("Track_Tracks_Id")] + public virtual ICollection<Track> Tracks { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/MusicAlbumMetadata.cs new file mode 100644 index 0000000000..b52ca65646 --- /dev/null +++ b/Jellyfin.Data/Entities/MusicAlbumMetadata.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class MusicAlbumMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected MusicAlbumMetadata() + { + Labels = new HashSet<Company>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static MusicAlbumMetadata CreateMusicAlbumMetadataUnsafe() + { + return new MusicAlbumMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_musicalbum0"></param> + public MusicAlbumMetadata(string title, string language, DateTime dateadded, DateTime datemodified, MusicAlbum _musicalbum0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_musicalbum0 == null) throw new ArgumentNullException(nameof(_musicalbum0)); + _musicalbum0.MusicAlbumMetadata.Add(this); + + this.Labels = new HashSet<Company>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_musicalbum0"></param> + public static MusicAlbumMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, MusicAlbum _musicalbum0) + { + return new MusicAlbumMetadata(title, language, dateadded, datemodified, _musicalbum0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Barcode + /// </summary> + protected string _Barcode; + /// <summary> + /// When provided in a partial class, allows value of Barcode to be changed before setting. + /// </summary> + partial void SetBarcode(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Barcode to be changed before returning. + /// </summary> + partial void GetBarcode(ref string result); + + /// <summary> + /// Max length = 255 + /// </summary> + [MaxLength(255)] + [StringLength(255)] + public string Barcode + { + get + { + string value = _Barcode; + GetBarcode(ref value); + return (_Barcode = value); + } + set + { + string oldValue = _Barcode; + SetBarcode(oldValue, ref value); + if (oldValue != value) + { + _Barcode = value; + } + } + } + + /// <summary> + /// Backing field for LabelNumber + /// </summary> + protected string _LabelNumber; + /// <summary> + /// When provided in a partial class, allows value of LabelNumber to be changed before setting. + /// </summary> + partial void SetLabelNumber(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of LabelNumber to be changed before returning. + /// </summary> + partial void GetLabelNumber(ref string result); + + /// <summary> + /// Max length = 255 + /// </summary> + [MaxLength(255)] + [StringLength(255)] + public string LabelNumber + { + get + { + string value = _LabelNumber; + GetLabelNumber(ref value); + return (_LabelNumber = value); + } + set + { + string oldValue = _LabelNumber; + SetLabelNumber(oldValue, ref value); + if (oldValue != value) + { + _LabelNumber = value; + } + } + } + + /// <summary> + /// Backing field for Country + /// </summary> + protected string _Country; + /// <summary> + /// When provided in a partial class, allows value of Country to be changed before setting. + /// </summary> + partial void SetCountry(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Country to be changed before returning. + /// </summary> + partial void GetCountry(ref string result); + + /// <summary> + /// Max length = 2 + /// </summary> + [MaxLength(2)] + [StringLength(2)] + public string Country + { + get + { + string value = _Country; + GetCountry(ref value); + return (_Country = value); + } + set + { + string oldValue = _Country; + SetCountry(oldValue, ref value); + if (oldValue != value) + { + _Country = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + [ForeignKey("Company_Labels_Id")] + public virtual ICollection<Company> Labels { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs new file mode 100644 index 0000000000..0b5b52cbd0 --- /dev/null +++ b/Jellyfin.Data/Entities/Permission.cs @@ -0,0 +1,144 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Runtime.CompilerServices; + +namespace Jellyfin.Data.Entities +{ + public partial class Permission + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Permission() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Permission CreatePermissionUnsafe() + { + return new Permission(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="kind"></param> + /// <param name="value"></param> + /// <param name="_user0"></param> + /// <param name="_group1"></param> + public Permission(Enums.PermissionKind kind, bool value, User _user0, Group _group1) + { + this.Kind = kind; + + this.Value = value; + + if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); + _user0.Permissions.Add(this); + + if (_group1 == null) throw new ArgumentNullException(nameof(_group1)); + _group1.GroupPermissions.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="kind"></param> + /// <param name="value"></param> + /// <param name="_user0"></param> + /// <param name="_group1"></param> + public static Permission Create(Enums.PermissionKind kind, bool value, User _user0, Group _group1) + { + return new Permission(kind, value, _user0, _group1); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Backing field for Kind + /// </summary> + protected Enums.PermissionKind _Kind; + /// <summary> + /// When provided in a partial class, allows value of Kind to be changed before setting. + /// </summary> + partial void SetKind(Enums.PermissionKind oldValue, ref Enums.PermissionKind newValue); + /// <summary> + /// When provided in a partial class, allows value of Kind to be changed before returning. + /// </summary> + partial void GetKind(ref Enums.PermissionKind result); + + /// <summary> + /// Required + /// </summary> + [Required] + public Enums.PermissionKind Kind + { + get + { + Enums.PermissionKind value = _Kind; + GetKind(ref value); + return (_Kind = value); + } + set + { + Enums.PermissionKind oldValue = _Kind; + SetKind(oldValue, ref value); + if (oldValue != value) + { + _Kind = value; + OnPropertyChanged(); + } + } + } + + /// <summary> + /// Required + /// </summary> + [Required] + public bool Value { get; set; } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + public virtual event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + } +} + diff --git a/Jellyfin.Data/Entities/Person.cs b/Jellyfin.Data/Entities/Person.cs new file mode 100644 index 0000000000..d893b7e394 --- /dev/null +++ b/Jellyfin.Data/Entities/Person.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Person + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Person() + { + Sources = new HashSet<MetadataProviderId>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Person CreatePersonUnsafe() + { + return new Person(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid"></param> + /// <param name="name"></param> + public Person(Guid urlid, string name, DateTime dateadded, DateTime datemodified) + { + this.UrlId = urlid; + + if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); + this.Name = name; + + this.Sources = new HashSet<MetadataProviderId>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid"></param> + /// <param name="name"></param> + public static Person Create(Guid urlid, string name, DateTime dateadded, DateTime datemodified) + { + return new Person(urlid, name, dateadded, datemodified); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for UrlId + /// </summary> + protected Guid _UrlId; + /// <summary> + /// When provided in a partial class, allows value of UrlId to be changed before setting. + /// </summary> + partial void SetUrlId(Guid oldValue, ref Guid newValue); + /// <summary> + /// When provided in a partial class, allows value of UrlId to be changed before returning. + /// </summary> + partial void GetUrlId(ref Guid result); + + /// <summary> + /// Required + /// </summary> + [Required] + public Guid UrlId + { + get + { + Guid value = _UrlId; + GetUrlId(ref value); + return (_UrlId = value); + } + set + { + Guid oldValue = _UrlId; + SetUrlId(oldValue, ref value); + if (oldValue != value) + { + _UrlId = value; + } + } + } + + /// <summary> + /// Backing field for Name + /// </summary> + protected string _Name; + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before setting. + /// </summary> + partial void SetName(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before returning. + /// </summary> + partial void GetName(ref string result); + + /// <summary> + /// Required, Max length = 1024 + /// </summary> + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name + { + get + { + string value = _Name; + GetName(ref value); + return (_Name = value); + } + set + { + string oldValue = _Name; + SetName(oldValue, ref value); + if (oldValue != value) + { + _Name = value; + } + } + } + + /// <summary> + /// Backing field for SourceId + /// </summary> + protected string _SourceId; + /// <summary> + /// When provided in a partial class, allows value of SourceId to be changed before setting. + /// </summary> + partial void SetSourceId(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of SourceId to be changed before returning. + /// </summary> + partial void GetSourceId(ref string result); + + /// <summary> + /// Max length = 255 + /// </summary> + [MaxLength(255)] + [StringLength(255)] + public string SourceId + { + get + { + string value = _SourceId; + GetSourceId(ref value); + return (_SourceId = value); + } + set + { + string oldValue = _SourceId; + SetSourceId(oldValue, ref value); + if (oldValue != value) + { + _SourceId = value; + } + } + } + + /// <summary> + /// Backing field for DateAdded + /// </summary> + protected DateTime _DateAdded; + /// <summary> + /// When provided in a partial class, allows value of DateAdded to be changed before setting. + /// </summary> + partial void SetDateAdded(DateTime oldValue, ref DateTime newValue); + /// <summary> + /// When provided in a partial class, allows value of DateAdded to be changed before returning. + /// </summary> + partial void GetDateAdded(ref DateTime result); + + /// <summary> + /// Required + /// </summary> + [Required] + public DateTime DateAdded + { + get + { + DateTime value = _DateAdded; + GetDateAdded(ref value); + return (_DateAdded = value); + } + internal set + { + DateTime oldValue = _DateAdded; + SetDateAdded(oldValue, ref value); + if (oldValue != value) + { + _DateAdded = value; + } + } + } + + /// <summary> + /// Backing field for DateModified + /// </summary> + protected DateTime _DateModified; + /// <summary> + /// When provided in a partial class, allows value of DateModified to be changed before setting. + /// </summary> + partial void SetDateModified(DateTime oldValue, ref DateTime newValue); + /// <summary> + /// When provided in a partial class, allows value of DateModified to be changed before returning. + /// </summary> + partial void GetDateModified(ref DateTime result); + + /// <summary> + /// Required + /// </summary> + [Required] + public DateTime DateModified + { + get + { + DateTime value = _DateModified; + GetDateModified(ref value); + return (_DateModified = value); + } + internal set + { + DateTime oldValue = _DateModified; + SetDateModified(oldValue, ref value); + if (oldValue != value) + { + _DateModified = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("MetadataProviderId_Sources_Id")] + public virtual ICollection<MetadataProviderId> Sources { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/PersonRole.cs b/Jellyfin.Data/Entities/PersonRole.cs new file mode 100644 index 0000000000..9bd12c7fb0 --- /dev/null +++ b/Jellyfin.Data/Entities/PersonRole.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class PersonRole + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected PersonRole() + { + // NOTE: This class has one-to-one associations with PersonRole. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + Sources = new HashSet<MetadataProviderId>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static PersonRole CreatePersonRoleUnsafe() + { + return new PersonRole(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="type"></param> + /// <param name="_metadata0"></param> + public PersonRole(Enums.PersonRoleType type, Metadata _metadata0) + { + // NOTE: This class has one-to-one associations with PersonRole. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + this.Type = type; + + if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0)); + _metadata0.PersonRoles.Add(this); + + this.Sources = new HashSet<MetadataProviderId>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="type"></param> + /// <param name="_metadata0"></param> + public static PersonRole Create(Enums.PersonRoleType type, Metadata _metadata0) + { + return new PersonRole(type, _metadata0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Role + /// </summary> + protected string _Role; + /// <summary> + /// When provided in a partial class, allows value of Role to be changed before setting. + /// </summary> + partial void SetRole(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Role to be changed before returning. + /// </summary> + partial void GetRole(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Role + { + get + { + string value = _Role; + GetRole(ref value); + return (_Role = value); + } + set + { + string oldValue = _Role; + SetRole(oldValue, ref value); + if (oldValue != value) + { + _Role = value; + } + } + } + + /// <summary> + /// Backing field for Type + /// </summary> + protected Enums.PersonRoleType _Type; + /// <summary> + /// When provided in a partial class, allows value of Type to be changed before setting. + /// </summary> + partial void SetType(Enums.PersonRoleType oldValue, ref Enums.PersonRoleType newValue); + /// <summary> + /// When provided in a partial class, allows value of Type to be changed before returning. + /// </summary> + partial void GetType(ref Enums.PersonRoleType result); + + /// <summary> + /// Required + /// </summary> + [Required] + public Enums.PersonRoleType Type + { + get + { + Enums.PersonRoleType value = _Type; + GetType(ref value); + return (_Type = value); + } + set + { + Enums.PersonRoleType oldValue = _Type; + SetType(oldValue, ref value); + if (oldValue != value) + { + _Type = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + /// <summary> + /// Required + /// </summary> + [ForeignKey("Person_Id")] + + public virtual Person Person { get; set; } + + [ForeignKey("Artwork_Artwork_Id")] + public virtual Artwork Artwork { get; set; } + + [ForeignKey("MetadataProviderId_Sources_Id")] + public virtual ICollection<MetadataProviderId> Sources { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/Photo.cs b/Jellyfin.Data/Entities/Photo.cs new file mode 100644 index 0000000000..7abe628913 --- /dev/null +++ b/Jellyfin.Data/Entities/Photo.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Photo : LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Photo() + { + PhotoMetadata = new HashSet<PhotoMetadata>(); + Releases = new HashSet<Release>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Photo CreatePhotoUnsafe() + { + return new Photo(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public Photo(Guid urlid, DateTime dateadded) + { + this.UrlId = urlid; + + this.PhotoMetadata = new HashSet<PhotoMetadata>(); + this.Releases = new HashSet<Release>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public static Photo Create(Guid urlid, DateTime dateadded) + { + return new Photo(urlid, dateadded); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("PhotoMetadata_PhotoMetadata_Id")] + public virtual ICollection<PhotoMetadata> PhotoMetadata { get; protected set; } + + [ForeignKey("Release_Releases_Id")] + public virtual ICollection<Release> Releases { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/PhotoMetadata.cs b/Jellyfin.Data/Entities/PhotoMetadata.cs new file mode 100644 index 0000000000..c5502f707a --- /dev/null +++ b/Jellyfin.Data/Entities/PhotoMetadata.cs @@ -0,0 +1,68 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class PhotoMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected PhotoMetadata() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static PhotoMetadata CreatePhotoMetadataUnsafe() + { + return new PhotoMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_photo0"></param> + public PhotoMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Photo _photo0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_photo0 == null) throw new ArgumentNullException(nameof(_photo0)); + _photo0.PhotoMetadata.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_photo0"></param> + public static PhotoMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Photo _photo0) + { + return new PhotoMetadata(title, language, dateadded, datemodified, _photo0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs new file mode 100644 index 0000000000..505f52e6b0 --- /dev/null +++ b/Jellyfin.Data/Entities/Preference.cs @@ -0,0 +1,107 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Preference + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Preference() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Preference CreatePreferenceUnsafe() + { + return new Preference(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="kind"></param> + /// <param name="value"></param> + /// <param name="_user0"></param> + /// <param name="_group1"></param> + public Preference(Enums.PreferenceKind kind, string value, User _user0, Group _group1) + { + this.Kind = kind; + + if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); + this.Value = value; + + if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); + _user0.Preferences.Add(this); + + if (_group1 == null) throw new ArgumentNullException(nameof(_group1)); + _group1.Preferences.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="kind"></param> + /// <param name="value"></param> + /// <param name="_user0"></param> + /// <param name="_group1"></param> + public static Preference Create(Enums.PreferenceKind kind, string value, User _user0, Group _group1) + { + return new Preference(kind, value, _user0, _group1); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Required + /// </summary> + [Required] + public Enums.PreferenceKind Kind { get; set; } + + /// <summary> + /// Required, Max length = 65535 + /// </summary> + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string Value { get; set; } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/ProviderMapping.cs b/Jellyfin.Data/Entities/ProviderMapping.cs new file mode 100644 index 0000000000..6197bd97b7 --- /dev/null +++ b/Jellyfin.Data/Entities/ProviderMapping.cs @@ -0,0 +1,123 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class ProviderMapping + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected ProviderMapping() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static ProviderMapping CreateProviderMappingUnsafe() + { + return new ProviderMapping(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="providername"></param> + /// <param name="providersecrets"></param> + /// <param name="providerdata"></param> + /// <param name="_user0"></param> + /// <param name="_group1"></param> + public ProviderMapping(string providername, string providersecrets, string providerdata, User _user0, Group _group1) + { + if (string.IsNullOrEmpty(providername)) throw new ArgumentNullException(nameof(providername)); + this.ProviderName = providername; + + if (string.IsNullOrEmpty(providersecrets)) throw new ArgumentNullException(nameof(providersecrets)); + this.ProviderSecrets = providersecrets; + + if (string.IsNullOrEmpty(providerdata)) throw new ArgumentNullException(nameof(providerdata)); + this.ProviderData = providerdata; + + if (_user0 == null) throw new ArgumentNullException(nameof(_user0)); + _user0.ProviderMappings.Add(this); + + if (_group1 == null) throw new ArgumentNullException(nameof(_group1)); + _group1.ProviderMappings.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="providername"></param> + /// <param name="providersecrets"></param> + /// <param name="providerdata"></param> + /// <param name="_user0"></param> + /// <param name="_group1"></param> + public static ProviderMapping Create(string providername, string providersecrets, string providerdata, User _user0, Group _group1) + { + return new ProviderMapping(providername, providersecrets, providerdata, _user0, _group1); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Required, Max length = 255 + /// </summary> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string ProviderName { get; set; } + + /// <summary> + /// Required, Max length = 65535 + /// </summary> + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string ProviderSecrets { get; set; } + + /// <summary> + /// Required, Max length = 65535 + /// </summary> + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string ProviderData { get; set; } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/Rating.cs b/Jellyfin.Data/Entities/Rating.cs new file mode 100644 index 0000000000..f70ea8b338 --- /dev/null +++ b/Jellyfin.Data/Entities/Rating.cs @@ -0,0 +1,187 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Rating + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Rating() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Rating CreateRatingUnsafe() + { + return new Rating(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="value"></param> + /// <param name="_metadata0"></param> + public Rating(double value, Metadata _metadata0) + { + this.Value = value; + + if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0)); + _metadata0.Ratings.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="value"></param> + /// <param name="_metadata0"></param> + public static Rating Create(double value, Metadata _metadata0) + { + return new Rating(value, _metadata0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Value + /// </summary> + protected double _Value; + /// <summary> + /// When provided in a partial class, allows value of Value to be changed before setting. + /// </summary> + partial void SetValue(double oldValue, ref double newValue); + /// <summary> + /// When provided in a partial class, allows value of Value to be changed before returning. + /// </summary> + partial void GetValue(ref double result); + + /// <summary> + /// Required + /// </summary> + [Required] + public double Value + { + get + { + double value = _Value; + GetValue(ref value); + return (_Value = value); + } + set + { + double oldValue = _Value; + SetValue(oldValue, ref value); + if (oldValue != value) + { + _Value = value; + } + } + } + + /// <summary> + /// Backing field for Votes + /// </summary> + protected int? _Votes; + /// <summary> + /// When provided in a partial class, allows value of Votes to be changed before setting. + /// </summary> + partial void SetVotes(int? oldValue, ref int? newValue); + /// <summary> + /// When provided in a partial class, allows value of Votes to be changed before returning. + /// </summary> + partial void GetVotes(ref int? result); + + public int? Votes + { + get + { + int? value = _Votes; + GetVotes(ref value); + return (_Votes = value); + } + set + { + int? oldValue = _Votes; + SetVotes(oldValue, ref value); + if (oldValue != value) + { + _Votes = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + /// <summary> + /// If this is NULL it's the internal user rating. + /// </summary> + [ForeignKey("RatingSource_RatingType_Id")] + public virtual RatingSource RatingType { get; set; } + + } +} + diff --git a/Jellyfin.Data/Entities/RatingSource.cs b/Jellyfin.Data/Entities/RatingSource.cs new file mode 100644 index 0000000000..070f1ae27e --- /dev/null +++ b/Jellyfin.Data/Entities/RatingSource.cs @@ -0,0 +1,231 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + /// <summary> + /// This is the entity to store review ratings, not age ratings + /// </summary> + public partial class RatingSource + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected RatingSource() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static RatingSource CreateRatingSourceUnsafe() + { + return new RatingSource(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="maximumvalue"></param> + /// <param name="minimumvalue"></param> + /// <param name="_rating0"></param> + public RatingSource(double maximumvalue, double minimumvalue, Rating _rating0) + { + this.MaximumValue = maximumvalue; + + this.MinimumValue = minimumvalue; + + if (_rating0 == null) throw new ArgumentNullException(nameof(_rating0)); + _rating0.RatingType = this; + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="maximumvalue"></param> + /// <param name="minimumvalue"></param> + /// <param name="_rating0"></param> + public static RatingSource Create(double maximumvalue, double minimumvalue, Rating _rating0) + { + return new RatingSource(maximumvalue, minimumvalue, _rating0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Name + /// </summary> + protected string _Name; + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before setting. + /// </summary> + partial void SetName(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before returning. + /// </summary> + partial void GetName(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Name + { + get + { + string value = _Name; + GetName(ref value); + return (_Name = value); + } + set + { + string oldValue = _Name; + SetName(oldValue, ref value); + if (oldValue != value) + { + _Name = value; + } + } + } + + /// <summary> + /// Backing field for MaximumValue + /// </summary> + protected double _MaximumValue; + /// <summary> + /// When provided in a partial class, allows value of MaximumValue to be changed before setting. + /// </summary> + partial void SetMaximumValue(double oldValue, ref double newValue); + /// <summary> + /// When provided in a partial class, allows value of MaximumValue to be changed before returning. + /// </summary> + partial void GetMaximumValue(ref double result); + + /// <summary> + /// Required + /// </summary> + [Required] + public double MaximumValue + { + get + { + double value = _MaximumValue; + GetMaximumValue(ref value); + return (_MaximumValue = value); + } + set + { + double oldValue = _MaximumValue; + SetMaximumValue(oldValue, ref value); + if (oldValue != value) + { + _MaximumValue = value; + } + } + } + + /// <summary> + /// Backing field for MinimumValue + /// </summary> + protected double _MinimumValue; + /// <summary> + /// When provided in a partial class, allows value of MinimumValue to be changed before setting. + /// </summary> + partial void SetMinimumValue(double oldValue, ref double newValue); + /// <summary> + /// When provided in a partial class, allows value of MinimumValue to be changed before returning. + /// </summary> + partial void GetMinimumValue(ref double result); + + /// <summary> + /// Required + /// </summary> + [Required] + public double MinimumValue + { + get + { + double value = _MinimumValue; + GetMinimumValue(ref value); + return (_MinimumValue = value); + } + set + { + double oldValue = _MinimumValue; + SetMinimumValue(oldValue, ref value); + if (oldValue != value) + { + _MinimumValue = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("MetadataProviderId_Source_Id")] + public virtual MetadataProviderId Source { get; set; } + + } +} + diff --git a/Jellyfin.Data/Entities/Release.cs b/Jellyfin.Data/Entities/Release.cs new file mode 100644 index 0000000000..d1928fcf7e --- /dev/null +++ b/Jellyfin.Data/Entities/Release.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Release + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Release() + { + MediaFiles = new HashSet<MediaFile>(); + Chapters = new HashSet<Chapter>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Release CreateReleaseUnsafe() + { + return new Release(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="name"></param> + /// <param name="_movie0"></param> + /// <param name="_episode1"></param> + /// <param name="_track2"></param> + /// <param name="_customitem3"></param> + /// <param name="_book4"></param> + /// <param name="_photo5"></param> + public Release(string name, Movie _movie0, Episode _episode1, Track _track2, CustomItem _customitem3, Book _book4, Photo _photo5) + { + if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); + this.Name = name; + + if (_movie0 == null) throw new ArgumentNullException(nameof(_movie0)); + _movie0.Releases.Add(this); + + if (_episode1 == null) throw new ArgumentNullException(nameof(_episode1)); + _episode1.Releases.Add(this); + + if (_track2 == null) throw new ArgumentNullException(nameof(_track2)); + _track2.Releases.Add(this); + + if (_customitem3 == null) throw new ArgumentNullException(nameof(_customitem3)); + _customitem3.Releases.Add(this); + + if (_book4 == null) throw new ArgumentNullException(nameof(_book4)); + _book4.Releases.Add(this); + + if (_photo5 == null) throw new ArgumentNullException(nameof(_photo5)); + _photo5.Releases.Add(this); + + this.MediaFiles = new HashSet<MediaFile>(); + this.Chapters = new HashSet<Chapter>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="name"></param> + /// <param name="_movie0"></param> + /// <param name="_episode1"></param> + /// <param name="_track2"></param> + /// <param name="_customitem3"></param> + /// <param name="_book4"></param> + /// <param name="_photo5"></param> + public static Release Create(string name, Movie _movie0, Episode _episode1, Track _track2, CustomItem _customitem3, Book _book4, Photo _photo5) + { + return new Release(name, _movie0, _episode1, _track2, _customitem3, _book4, _photo5); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Id + /// </summary> + internal int _Id; + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before setting. + /// </summary> + partial void SetId(int oldValue, ref int newValue); + /// <summary> + /// When provided in a partial class, allows value of Id to be changed before returning. + /// </summary> + partial void GetId(ref int result); + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id + { + get + { + int value = _Id; + GetId(ref value); + return (_Id = value); + } + protected set + { + int oldValue = _Id; + SetId(oldValue, ref value); + if (oldValue != value) + { + _Id = value; + } + } + } + + /// <summary> + /// Backing field for Name + /// </summary> + protected string _Name; + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before setting. + /// </summary> + partial void SetName(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Name to be changed before returning. + /// </summary> + partial void GetName(ref string result); + + /// <summary> + /// Required, Max length = 1024 + /// </summary> + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name + { + get + { + string value = _Name; + GetName(ref value); + return (_Name = value); + } + set + { + string oldValue = _Name; + SetName(oldValue, ref value); + if (oldValue != value) + { + _Name = value; + } + } + } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("MediaFile_MediaFiles_Id")] + public virtual ICollection<MediaFile> MediaFiles { get; protected set; } + + [ForeignKey("Chapter_Chapters_Id")] + public virtual ICollection<Chapter> Chapters { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/Season.cs b/Jellyfin.Data/Entities/Season.cs new file mode 100644 index 0000000000..96e89cde05 --- /dev/null +++ b/Jellyfin.Data/Entities/Season.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Season : LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Season() + { + // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + SeasonMetadata = new HashSet<SeasonMetadata>(); + Episodes = new HashSet<Episode>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Season CreateSeasonUnsafe() + { + return new Season(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + /// <param name="_series0"></param> + public Season(Guid urlid, DateTime dateadded, Series _series0) + { + // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + this.UrlId = urlid; + + if (_series0 == null) throw new ArgumentNullException(nameof(_series0)); + _series0.Seasons.Add(this); + + this.SeasonMetadata = new HashSet<SeasonMetadata>(); + this.Episodes = new HashSet<Episode>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + /// <param name="_series0"></param> + public static Season Create(Guid urlid, DateTime dateadded, Series _series0) + { + return new Season(urlid, dateadded, _series0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for SeasonNumber + /// </summary> + protected int? _SeasonNumber; + /// <summary> + /// When provided in a partial class, allows value of SeasonNumber to be changed before setting. + /// </summary> + partial void SetSeasonNumber(int? oldValue, ref int? newValue); + /// <summary> + /// When provided in a partial class, allows value of SeasonNumber to be changed before returning. + /// </summary> + partial void GetSeasonNumber(ref int? result); + + public int? SeasonNumber + { + get + { + int? value = _SeasonNumber; + GetSeasonNumber(ref value); + return (_SeasonNumber = value); + } + set + { + int? oldValue = _SeasonNumber; + SetSeasonNumber(oldValue, ref value); + if (oldValue != value) + { + _SeasonNumber = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("SeasonMetadata_SeasonMetadata_Id")] + public virtual ICollection<SeasonMetadata> SeasonMetadata { get; protected set; } + + [ForeignKey("Episode_Episodes_Id")] + public virtual ICollection<Episode> Episodes { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/SeasonMetadata.cs b/Jellyfin.Data/Entities/SeasonMetadata.cs new file mode 100644 index 0000000000..64ecbfbfac --- /dev/null +++ b/Jellyfin.Data/Entities/SeasonMetadata.cs @@ -0,0 +1,106 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class SeasonMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected SeasonMetadata() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static SeasonMetadata CreateSeasonMetadataUnsafe() + { + return new SeasonMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_season0"></param> + public SeasonMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Season _season0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_season0 == null) throw new ArgumentNullException(nameof(_season0)); + _season0.SeasonMetadata.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_season0"></param> + public static SeasonMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Season _season0) + { + return new SeasonMetadata(title, language, dateadded, datemodified, _season0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Outline + /// </summary> + protected string _Outline; + /// <summary> + /// When provided in a partial class, allows value of Outline to be changed before setting. + /// </summary> + partial void SetOutline(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Outline to be changed before returning. + /// </summary> + partial void GetOutline(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Outline + { + get + { + string value = _Outline; + GetOutline(ref value); + return (_Outline = value); + } + set + { + string oldValue = _Outline; + SetOutline(oldValue, ref value); + if (oldValue != value) + { + _Outline = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/Series.cs b/Jellyfin.Data/Entities/Series.cs new file mode 100644 index 0000000000..097b9958e3 --- /dev/null +++ b/Jellyfin.Data/Entities/Series.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Series : LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Series() + { + SeriesMetadata = new HashSet<SeriesMetadata>(); + Seasons = new HashSet<Season>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Series CreateSeriesUnsafe() + { + return new Series(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public Series(Guid urlid, DateTime dateadded) + { + this.UrlId = urlid; + + this.SeriesMetadata = new HashSet<SeriesMetadata>(); + this.Seasons = new HashSet<Season>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + public static Series Create(Guid urlid, DateTime dateadded) + { + return new Series(urlid, dateadded); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for AirsDayOfWeek + /// </summary> + protected Enums.Weekday? _AirsDayOfWeek; + /// <summary> + /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before setting. + /// </summary> + partial void SetAirsDayOfWeek(Enums.Weekday? oldValue, ref Enums.Weekday? newValue); + /// <summary> + /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before returning. + /// </summary> + partial void GetAirsDayOfWeek(ref Enums.Weekday? result); + + public Enums.Weekday? AirsDayOfWeek + { + get + { + Enums.Weekday? value = _AirsDayOfWeek; + GetAirsDayOfWeek(ref value); + return (_AirsDayOfWeek = value); + } + set + { + Enums.Weekday? oldValue = _AirsDayOfWeek; + SetAirsDayOfWeek(oldValue, ref value); + if (oldValue != value) + { + _AirsDayOfWeek = value; + } + } + } + + /// <summary> + /// Backing field for AirsTime + /// </summary> + protected DateTimeOffset? _AirsTime; + /// <summary> + /// When provided in a partial class, allows value of AirsTime to be changed before setting. + /// </summary> + partial void SetAirsTime(DateTimeOffset? oldValue, ref DateTimeOffset? newValue); + /// <summary> + /// When provided in a partial class, allows value of AirsTime to be changed before returning. + /// </summary> + partial void GetAirsTime(ref DateTimeOffset? result); + + /// <summary> + /// The time the show airs, ignore the date portion + /// </summary> + public DateTimeOffset? AirsTime + { + get + { + DateTimeOffset? value = _AirsTime; + GetAirsTime(ref value); + return (_AirsTime = value); + } + set + { + DateTimeOffset? oldValue = _AirsTime; + SetAirsTime(oldValue, ref value); + if (oldValue != value) + { + _AirsTime = value; + } + } + } + + /// <summary> + /// Backing field for FirstAired + /// </summary> + protected DateTimeOffset? _FirstAired; + /// <summary> + /// When provided in a partial class, allows value of FirstAired to be changed before setting. + /// </summary> + partial void SetFirstAired(DateTimeOffset? oldValue, ref DateTimeOffset? newValue); + /// <summary> + /// When provided in a partial class, allows value of FirstAired to be changed before returning. + /// </summary> + partial void GetFirstAired(ref DateTimeOffset? result); + + public DateTimeOffset? FirstAired + { + get + { + DateTimeOffset? value = _FirstAired; + GetFirstAired(ref value); + return (_FirstAired = value); + } + set + { + DateTimeOffset? oldValue = _FirstAired; + SetFirstAired(oldValue, ref value); + if (oldValue != value) + { + _FirstAired = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("SeriesMetadata_SeriesMetadata_Id")] + public virtual ICollection<SeriesMetadata> SeriesMetadata { get; protected set; } + + [ForeignKey("Season_Seasons_Id")] + public virtual ICollection<Season> Seasons { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/SeriesMetadata.cs b/Jellyfin.Data/Entities/SeriesMetadata.cs new file mode 100644 index 0000000000..52691783f6 --- /dev/null +++ b/Jellyfin.Data/Entities/SeriesMetadata.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class SeriesMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected SeriesMetadata() + { + Networks = new HashSet<Company>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static SeriesMetadata CreateSeriesMetadataUnsafe() + { + return new SeriesMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_series0"></param> + public SeriesMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Series _series0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_series0 == null) throw new ArgumentNullException(nameof(_series0)); + _series0.SeriesMetadata.Add(this); + + this.Networks = new HashSet<Company>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_series0"></param> + public static SeriesMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Series _series0) + { + return new SeriesMetadata(title, language, dateadded, datemodified, _series0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for Outline + /// </summary> + protected string _Outline; + /// <summary> + /// When provided in a partial class, allows value of Outline to be changed before setting. + /// </summary> + partial void SetOutline(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Outline to be changed before returning. + /// </summary> + partial void GetOutline(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Outline + { + get + { + string value = _Outline; + GetOutline(ref value); + return (_Outline = value); + } + set + { + string oldValue = _Outline; + SetOutline(oldValue, ref value); + if (oldValue != value) + { + _Outline = value; + } + } + } + + /// <summary> + /// Backing field for Plot + /// </summary> + protected string _Plot; + /// <summary> + /// When provided in a partial class, allows value of Plot to be changed before setting. + /// </summary> + partial void SetPlot(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Plot to be changed before returning. + /// </summary> + partial void GetPlot(ref string result); + + /// <summary> + /// Max length = 65535 + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string Plot + { + get + { + string value = _Plot; + GetPlot(ref value); + return (_Plot = value); + } + set + { + string oldValue = _Plot; + SetPlot(oldValue, ref value); + if (oldValue != value) + { + _Plot = value; + } + } + } + + /// <summary> + /// Backing field for Tagline + /// </summary> + protected string _Tagline; + /// <summary> + /// When provided in a partial class, allows value of Tagline to be changed before setting. + /// </summary> + partial void SetTagline(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Tagline to be changed before returning. + /// </summary> + partial void GetTagline(ref string result); + + /// <summary> + /// Max length = 1024 + /// </summary> + [MaxLength(1024)] + [StringLength(1024)] + public string Tagline + { + get + { + string value = _Tagline; + GetTagline(ref value); + return (_Tagline = value); + } + set + { + string oldValue = _Tagline; + SetTagline(oldValue, ref value); + if (oldValue != value) + { + _Tagline = value; + } + } + } + + /// <summary> + /// Backing field for Country + /// </summary> + protected string _Country; + /// <summary> + /// When provided in a partial class, allows value of Country to be changed before setting. + /// </summary> + partial void SetCountry(string oldValue, ref string newValue); + /// <summary> + /// When provided in a partial class, allows value of Country to be changed before returning. + /// </summary> + partial void GetCountry(ref string result); + + /// <summary> + /// Max length = 2 + /// </summary> + [MaxLength(2)] + [StringLength(2)] + public string Country + { + get + { + string value = _Country; + GetCountry(ref value); + return (_Country = value); + } + set + { + string oldValue = _Country; + SetCountry(oldValue, ref value); + if (oldValue != value) + { + _Country = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("Company_Networks_Id")] + public virtual ICollection<Company> Networks { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/Track.cs b/Jellyfin.Data/Entities/Track.cs new file mode 100644 index 0000000000..079d73d2bf --- /dev/null +++ b/Jellyfin.Data/Entities/Track.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class Track : LibraryItem + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected Track() + { + // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + Releases = new HashSet<Release>(); + TrackMetadata = new HashSet<TrackMetadata>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static Track CreateTrackUnsafe() + { + return new Track(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + /// <param name="_musicalbum0"></param> + public Track(Guid urlid, DateTime dateadded, MusicAlbum _musicalbum0) + { + // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. + // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. + + this.UrlId = urlid; + + if (_musicalbum0 == null) throw new ArgumentNullException(nameof(_musicalbum0)); + _musicalbum0.Tracks.Add(this); + + this.Releases = new HashSet<Release>(); + this.TrackMetadata = new HashSet<TrackMetadata>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> + /// <param name="_musicalbum0"></param> + public static Track Create(Guid urlid, DateTime dateadded, MusicAlbum _musicalbum0) + { + return new Track(urlid, dateadded, _musicalbum0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Backing field for TrackNumber + /// </summary> + protected int? _TrackNumber; + /// <summary> + /// When provided in a partial class, allows value of TrackNumber to be changed before setting. + /// </summary> + partial void SetTrackNumber(int? oldValue, ref int? newValue); + /// <summary> + /// When provided in a partial class, allows value of TrackNumber to be changed before returning. + /// </summary> + partial void GetTrackNumber(ref int? result); + + public int? TrackNumber + { + get + { + int? value = _TrackNumber; + GetTrackNumber(ref value); + return (_TrackNumber = value); + } + set + { + int? oldValue = _TrackNumber; + SetTrackNumber(oldValue, ref value); + if (oldValue != value) + { + _TrackNumber = value; + } + } + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + [ForeignKey("Release_Releases_Id")] + public virtual ICollection<Release> Releases { get; protected set; } + + [ForeignKey("TrackMetadata_TrackMetadata_Id")] + public virtual ICollection<TrackMetadata> TrackMetadata { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Entities/TrackMetadata.cs b/Jellyfin.Data/Entities/TrackMetadata.cs new file mode 100644 index 0000000000..86c9161f6e --- /dev/null +++ b/Jellyfin.Data/Entities/TrackMetadata.cs @@ -0,0 +1,68 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class TrackMetadata : Metadata + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected TrackMetadata() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static TrackMetadata CreateTrackMetadataUnsafe() + { + return new TrackMetadata(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_track0"></param> + public TrackMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Track _track0) + { + if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title)); + this.Title = title; + + if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language)); + this.Language = language; + + if (_track0 == null) throw new ArgumentNullException(nameof(_track0)); + _track0.TrackMetadata.Add(this); + + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="title">The title or name of the object</param> + /// <param name="language">ISO-639-3 3-character language codes</param> + /// <param name="_track0"></param> + public static TrackMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Track _track0) + { + return new TrackMetadata(title, language, dateadded, datemodified, _track0); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /************************************************************************* + * Navigation properties + *************************************************************************/ + + } +} + diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs new file mode 100644 index 0000000000..a81d5215bb --- /dev/null +++ b/Jellyfin.Data/Entities/User.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities +{ + public partial class User + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected User() + { + Groups = new HashSet<Group>(); + Permissions = new HashSet<Permission>(); + ProviderMappings = new HashSet<ProviderMapping>(); + Preferences = new HashSet<Preference>(); + + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static User CreateUserUnsafe() + { + return new User(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="username"></param> + /// <param name="mustupdatepassword"></param> + /// <param name="audiolanguagepreference"></param> + /// <param name="authenticationproviderid"></param> + /// <param name="invalidloginattemptcount"></param> + /// <param name="subtitlemode"></param> + /// <param name="playdefaultaudiotrack"></param> + public User(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack) + { + if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username)); + this.Username = username; + + this.MustUpdatePassword = mustupdatepassword; + + if (string.IsNullOrEmpty(audiolanguagepreference)) throw new ArgumentNullException(nameof(audiolanguagepreference)); + this.AudioLanguagePreference = audiolanguagepreference; + + if (string.IsNullOrEmpty(authenticationproviderid)) throw new ArgumentNullException(nameof(authenticationproviderid)); + this.AuthenticationProviderId = authenticationproviderid; + + this.InvalidLoginAttemptCount = invalidloginattemptcount; + + if (string.IsNullOrEmpty(subtitlemode)) throw new ArgumentNullException(nameof(subtitlemode)); + this.SubtitleMode = subtitlemode; + + this.PlayDefaultAudioTrack = playdefaultaudiotrack; + + this.Groups = new HashSet<Group>(); + this.Permissions = new HashSet<Permission>(); + this.ProviderMappings = new HashSet<ProviderMapping>(); + this.Preferences = new HashSet<Preference>(); + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="username"></param> + /// <param name="mustupdatepassword"></param> + /// <param name="audiolanguagepreference"></param> + /// <param name="authenticationproviderid"></param> + /// <param name="invalidloginattemptcount"></param> + /// <param name="subtitlemode"></param> + /// <param name="playdefaultaudiotrack"></param> + public static User Create(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack) + { + return new User(username, mustupdatepassword, audiolanguagepreference, authenticationproviderid, invalidloginattemptcount, subtitlemode, playdefaultaudiotrack); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Required, Max length = 255 + /// </summary> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string Username { get; set; } + + /// <summary> + /// Max length = 65535 + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string Password { get; set; } + + /// <summary> + /// Required + /// </summary> + [Required] + public bool MustUpdatePassword { get; set; } + + /// <summary> + /// Required, Max length = 255 + /// </summary> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string AudioLanguagePreference { get; set; } + + /// <summary> + /// Required, Max length = 255 + /// </summary> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string AuthenticationProviderId { get; set; } + + /// <summary> + /// Max length = 65535 + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string GroupedFolders { get; set; } + + /// <summary> + /// Required + /// </summary> + [Required] + public int InvalidLoginAttemptCount { get; set; } + + /// <summary> + /// Max length = 65535 + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string LatestItemExcludes { get; set; } + + public int? LoginAttemptsBeforeLockout { get; set; } + + /// <summary> + /// Max length = 65535 + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string MyMediaExcludes { get; set; } + + /// <summary> + /// Max length = 65535 + /// </summary> + [MaxLength(65535)] + [StringLength(65535)] + public string OrderedViews { get; set; } + + /// <summary> + /// Required, Max length = 255 + /// </summary> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string SubtitleMode { get; set; } + + /// <summary> + /// Required + /// </summary> + [Required] + public bool PlayDefaultAudioTrack { get; set; } + + /// <summary> + /// Max length = 255 + /// </summary> + [MaxLength(255)] + [StringLength(255)] + public string SubtitleLanguagePrefernce { get; set; } + + public bool? DisplayMissingEpisodes { get; set; } + + public bool? DisplayCollectionsView { get; set; } + + public bool? HidePlayedInLatest { get; set; } + + public bool? RememberAudioSelections { get; set; } + + public bool? RememberSubtitleSelections { get; set; } + + public bool? EnableNextEpisodeAutoPlay { get; set; } + + public bool? EnableUserPreferenceAccess { get; set; } + + /// <summary> + /// Required, ConcurrenyToken + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + + /************************************************************************* + * Navigation properties + *************************************************************************/ + [ForeignKey("Group_Groups_Id")] + public virtual ICollection<Group> Groups { get; protected set; } + + [ForeignKey("Permission_Permissions_Id")] + public virtual ICollection<Permission> Permissions { get; protected set; } + + [ForeignKey("ProviderMapping_ProviderMappings_Id")] + public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; } + + [ForeignKey("Preference_Preferences_Id")] + public virtual ICollection<Preference> Preferences { get; protected set; } + + } +} + diff --git a/Jellyfin.Data/Enums/ArtKind.cs b/Jellyfin.Data/Enums/ArtKind.cs new file mode 100644 index 0000000000..6b69d68b28 --- /dev/null +++ b/Jellyfin.Data/Enums/ArtKind.cs @@ -0,0 +1,11 @@ +namespace Jellyfin.Data.Enums +{ + public enum ArtKind + { + Other, + Poster, + Banner, + Thumbnail, + Logo + } +} diff --git a/Jellyfin.Data/Enums/MediaFileKind.cs b/Jellyfin.Data/Enums/MediaFileKind.cs new file mode 100644 index 0000000000..12f48c5589 --- /dev/null +++ b/Jellyfin.Data/Enums/MediaFileKind.cs @@ -0,0 +1,11 @@ +namespace Jellyfin.Data.Enums +{ + public enum MediaFileKind + { + Main, + Sidecar, + AdditionalPart, + AlternativeFormat, + AdditionalStream + } +} diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs new file mode 100644 index 0000000000..1506471e86 --- /dev/null +++ b/Jellyfin.Data/Enums/PermissionKind.cs @@ -0,0 +1,26 @@ +namespace Jellyfin.Data.Enums +{ + public enum PermissionKind + { + IsAdministrator, + IsHidden, + IsDisabled, + BlockUnrateditems, + EnbleSharedDeviceControl, + EnableRemoteAccess, + EnableLiveTvManagement, + EnableLiveTvAccess, + EnableMediaPlayback, + EnableAudioPlaybackTranscoding, + EnableVideoPlaybackTranscoding, + EnableContentDeletion, + EnableContentDownloading, + EnableSyncTranscoding, + EnableMediaConversion, + EnableAllDevices, + EnableAllChannels, + EnableAllFolders, + EnablePublicSharing, + AccessSchedules + } +} diff --git a/Jellyfin.Data/Enums/PersonRoleType.cs b/Jellyfin.Data/Enums/PersonRoleType.cs new file mode 100644 index 0000000000..6e52f2c851 --- /dev/null +++ b/Jellyfin.Data/Enums/PersonRoleType.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Data.Enums +{ + public enum PersonRoleType + { + Other, + Director, + Artist, + OriginalArtist, + Actor, + VoiceActor, + Producer, + Remixer, + Conductor, + Composer, + Author, + Editor + } +} diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Data/Enums/PreferenceKind.cs new file mode 100644 index 0000000000..cd2cb791af --- /dev/null +++ b/Jellyfin.Data/Enums/PreferenceKind.cs @@ -0,0 +1,13 @@ +namespace Jellyfin.Data.Enums +{ + public enum PreferenceKind + { + MaxParentalRating, + BlockedTags, + RemoteClientBitrateLimit, + EnabledDevices, + EnabledChannels, + EnabledFolders, + EnableContentDeletionFromFolders + } +} diff --git a/Jellyfin.Data/Enums/Weekday.cs b/Jellyfin.Data/Enums/Weekday.cs new file mode 100644 index 0000000000..b80a03a330 --- /dev/null +++ b/Jellyfin.Data/Enums/Weekday.cs @@ -0,0 +1,13 @@ +namespace Jellyfin.Data.Enums +{ + public enum Weekday + { + Sunday, + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday + } +} diff --git a/Jellyfin.Data/ISavingChanges.cs b/Jellyfin.Data/ISavingChanges.cs new file mode 100644 index 0000000000..f392dae6a0 --- /dev/null +++ b/Jellyfin.Data/ISavingChanges.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 + +namespace Jellyfin.Data +{ + public interface ISavingChanges + { + void OnSavingChanges(); + } +} diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj new file mode 100644 index 0000000000..9157c3ead9 --- /dev/null +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -0,0 +1,26 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + + <!-- Code analysers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.4" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" /> + </ItemGroup> + +</Project> diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index d0a99e1e28..a6e1f490ad 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -1,10 +1,16 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{154872D9-6C12-4007-96E3-8F70A58386CE}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs index 5084fd211c..7eed5f4f79 100644 --- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs +++ b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs @@ -26,7 +26,7 @@ namespace Jellyfin.Drawing.Skia { paint.Color = SKColor.Parse("#CC00A4DC"); paint.Style = SKPaintStyle.Fill; - canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); + canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); } using (var paint = new SKPaint()) @@ -39,16 +39,13 @@ namespace Jellyfin.Drawing.Skia // or: // var emojiChar = 0x1F680; - var text = "✔️"; - var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32); + const string Text = "✔️"; + var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32); // ask the font manager for a font with that character - var fontManager = SKFontManager.Default; - var emojiTypeface = fontManager.MatchCharacter(emojiChar); + paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar); - paint.Typeface = emojiTypeface; - - canvas.DrawText(text, (float)x - 20, OffsetFromTopRightCorner + 12, paint); + canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint); } } } diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index a67118f188..5c7462ee29 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -78,12 +78,21 @@ namespace Jellyfin.Drawing.Skia => new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; /// <summary> - /// Test to determine if the native lib is available. + /// Check if the native lib is available. /// </summary> - public static void TestSkia() + /// <returns>True if the native lib is available, otherwise false.</returns> + public static bool IsNativeLibAvailable() { - // test an operation that requires the native library - SKPMColor.PreMultiply(SKColors.Black); + try + { + // test an operation that requires the native library + SKPMColor.PreMultiply(SKColors.Black); + return true; + } + catch (Exception) + { + return false; + } } private static bool IsTransparent(SKColor color) @@ -205,11 +214,6 @@ namespace Jellyfin.Drawing.Skia /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception> public ImageDimensions GetImageSize(string path) { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - if (!File.Exists(path)) { throw new FileNotFoundException("File not found", path); @@ -297,7 +301,7 @@ namespace Jellyfin.Drawing.Skia /// <param name="orientation">The orientation of the image.</param> /// <param name="origin">The detected origin of the image.</param> /// <returns>The resulting bitmap of the image.</returns> - internal SKBitmap Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) + internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) { if (!File.Exists(path)) { @@ -348,12 +352,17 @@ namespace Jellyfin.Drawing.Skia return resultBitmap; } - private SKBitmap GetBitmap(string path, bool cropWhitespace, bool forceAnalyzeBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) + private SKBitmap? GetBitmap(string path, bool cropWhitespace, bool forceAnalyzeBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) { if (cropWhitespace) { using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin)) { + if (bitmap == null) + { + return null; + } + return CropWhiteSpace(bitmap); } } @@ -361,13 +370,11 @@ namespace Jellyfin.Drawing.Skia return Decode(path, forceAnalyzeBitmap, orientation, out origin); } - private SKBitmap GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation) + private SKBitmap? GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation) { - SKEncodedOrigin origin; - if (autoOrient) { - var bitmap = GetBitmap(path, cropWhitespace, true, orientation, out origin); + var bitmap = GetBitmap(path, cropWhitespace, true, orientation, out var origin); if (bitmap != null && origin != SKEncodedOrigin.TopLeft) { @@ -380,7 +387,7 @@ namespace Jellyfin.Drawing.Skia return bitmap; } - return GetBitmap(path, cropWhitespace, false, orientation, out origin); + return GetBitmap(path, cropWhitespace, false, orientation, out _); } private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) @@ -517,14 +524,14 @@ namespace Jellyfin.Drawing.Skia /// <inheritdoc/> public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) { - if (string.IsNullOrWhiteSpace(inputPath)) + if (inputPath.Length == 0) { - throw new ArgumentNullException(nameof(inputPath)); + throw new ArgumentException("String can't be empty.", nameof(inputPath)); } - if (string.IsNullOrWhiteSpace(inputPath)) + if (outputPath.Length == 0) { - throw new ArgumentNullException(nameof(outputPath)); + throw new ArgumentException("String can't be empty.", nameof(outputPath)); } var skiaOutputFormat = GetImageFormat(selectedOutputFormat); @@ -538,7 +545,7 @@ namespace Jellyfin.Drawing.Skia { if (bitmap == null) { - throw new ArgumentOutOfRangeException($"Skia unable to read image {inputPath}"); + throw new InvalidDataException($"Skia unable to read image {inputPath}"); } var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 0735ef194a..61bef90ec5 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -120,13 +120,13 @@ namespace Jellyfin.Drawing.Skia } // resize to the same aspect as the original - int iWidth = (int)Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height); + int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height); using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType)) { currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High); // crop image - int ix = (int)Math.Abs((iWidth - iSlice) / 2); + int ix = Math.Abs((iWidth - iSlice) / 2); using (var image = SKImage.FromBitmap(resizeBitmap)) using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight))) { @@ -141,10 +141,10 @@ namespace Jellyfin.Drawing.Skia return bitmap; } - private SKBitmap GetNextValidImage(string[] paths, int currentIndex, out int newIndex) + private SKBitmap? GetNextValidImage(string[] paths, int currentIndex, out int newIndex) { var imagesTested = new Dictionary<int, int>(); - SKBitmap bitmap = null; + SKBitmap? bitmap = null; while (imagesTested.Count < paths.Length) { @@ -153,7 +153,7 @@ namespace Jellyfin.Drawing.Skia currentIndex = 0; } - bitmap = _skiaEncoder.Decode(paths[currentIndex], false, null, out var origin); + bitmap = _skiaEncoder.Decode(paths[currentIndex], false, null, out _); imagesTested[currentIndex] = 0; diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs index a10fff9dfe..cf3dbde2c0 100644 --- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs +++ b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs @@ -32,7 +32,7 @@ namespace Jellyfin.Drawing.Skia { paint.Color = SKColor.Parse("#CC00A4DC"); paint.Style = SKPaintStyle.Fill; - canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); + canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); } using (var paint = new SKPaint()) @@ -61,7 +61,7 @@ namespace Jellyfin.Drawing.Skia paint.TextSize = 18; } - canvas.DrawText(text, (float)x, y, paint); + canvas.DrawText(text, x, y, paint); } } } diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs new file mode 100644 index 0000000000..65ceee32bf --- /dev/null +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -0,0 +1,102 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Querying; + +namespace Jellyfin.Server.Implementations.Activity +{ + /// <summary> + /// Manages the storage and retrieval of <see cref="ActivityLog"/> instances. + /// </summary> + public class ActivityManager : IActivityManager + { + private readonly JellyfinDbProvider _provider; + + /// <summary> + /// Initializes a new instance of the <see cref="ActivityManager"/> class. + /// </summary> + /// <param name="provider">The Jellyfin database provider.</param> + public ActivityManager(JellyfinDbProvider provider) + { + _provider = provider; + } + + /// <inheritdoc/> + public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; + + /// <inheritdoc/> + public void Create(ActivityLog entry) + { + using var dbContext = _provider.CreateContext(); + dbContext.ActivityLogs.Add(entry); + dbContext.SaveChanges(); + + EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry))); + } + + /// <inheritdoc/> + public async Task CreateAsync(ActivityLog entry) + { + using var dbContext = _provider.CreateContext(); + await dbContext.ActivityLogs.AddAsync(entry); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry))); + } + + /// <inheritdoc/> + public QueryResult<ActivityLogEntry> GetPagedResult( + Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func, + int? startIndex, + int? limit) + { + using var dbContext = _provider.CreateContext(); + + var query = func(dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated)); + + if (startIndex.HasValue) + { + query = query.Skip(startIndex.Value); + } + + if (limit.HasValue) + { + query = query.Take(limit.Value); + } + + // This converts the objects from the new database model to the old for compatibility with the existing API. + var list = query.Select(ConvertToOldModel).ToList(); + + return new QueryResult<ActivityLogEntry> + { + Items = list, + TotalRecordCount = func(dbContext.ActivityLogs).Count() + }; + } + + /// <inheritdoc/> + public QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit) + { + return GetPagedResult(logs => logs, startIndex, limit); + } + + private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) + { + return new ActivityLogEntry + { + Id = entry.Id, + Name = entry.Name, + Overview = entry.Overview, + ShortOverview = entry.ShortOverview, + Type = entry.Type, + ItemId = entry.ItemId, + UserId = entry.UserId, + Date = entry.DateCreated, + Severity = entry.LogSeverity + }; + } + } +} diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj new file mode 100644 index 0000000000..8486fc2dfb --- /dev/null +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -0,0 +1,45 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.1</TargetFramework> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + + <!-- Code analysers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <ItemGroup> + <Compile Include="..\SharedVersion.cs" /> + <Compile Remove="Migrations\20200430214405_InitialSchema.cs" /> + <Compile Remove="Migrations\20200430214405_InitialSchema.Designer.cs" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.4"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.4"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" /> + <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> + <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + </ItemGroup> + +</Project> diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs new file mode 100644 index 0000000000..ec09a619f2 --- /dev/null +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -0,0 +1,120 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1201 // Constuctors should not follow properties +#pragma warning disable SA1516 // Elements should be followed by a blank line +#pragma warning disable SA1623 // Property's documentation should begin with gets or sets +#pragma warning disable SA1629 // Documentation should end with a period +#pragma warning disable SA1648 // Inheritdoc should be used with inheriting class + +using System.Linq; +using Jellyfin.Data; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations +{ + /// <inheritdoc/> + public partial class JellyfinDb : DbContext + { + public virtual DbSet<ActivityLog> ActivityLogs { get; set; } + /*public virtual DbSet<Artwork> Artwork { get; set; } + public virtual DbSet<Book> Books { get; set; } + public virtual DbSet<BookMetadata> BookMetadata { get; set; } + public virtual DbSet<Chapter> Chapters { get; set; } + public virtual DbSet<Collection> Collections { get; set; } + public virtual DbSet<CollectionItem> CollectionItems { get; set; } + public virtual DbSet<Company> Companies { get; set; } + public virtual DbSet<CompanyMetadata> CompanyMetadata { get; set; } + public virtual DbSet<CustomItem> CustomItems { get; set; } + public virtual DbSet<CustomItemMetadata> CustomItemMetadata { get; set; } + public virtual DbSet<Episode> Episodes { get; set; } + public virtual DbSet<EpisodeMetadata> EpisodeMetadata { get; set; } + public virtual DbSet<Genre> Genres { get; set; } + public virtual DbSet<Group> Groups { get; set; } + public virtual DbSet<Library> Libraries { get; set; } + public virtual DbSet<LibraryItem> LibraryItems { get; set; } + public virtual DbSet<LibraryRoot> LibraryRoot { get; set; } + public virtual DbSet<MediaFile> MediaFiles { get; set; } + public virtual DbSet<MediaFileStream> MediaFileStream { get; set; } + public virtual DbSet<Metadata> Metadata { get; set; } + public virtual DbSet<MetadataProvider> MetadataProviders { get; set; } + public virtual DbSet<MetadataProviderId> MetadataProviderIds { get; set; } + public virtual DbSet<Movie> Movies { get; set; } + public virtual DbSet<MovieMetadata> MovieMetadata { get; set; } + public virtual DbSet<MusicAlbum> MusicAlbums { get; set; } + public virtual DbSet<MusicAlbumMetadata> MusicAlbumMetadata { get; set; } + public virtual DbSet<Permission> Permissions { get; set; } + public virtual DbSet<Person> People { get; set; } + public virtual DbSet<PersonRole> PersonRoles { get; set; } + public virtual DbSet<Photo> Photo { get; set; } + public virtual DbSet<PhotoMetadata> PhotoMetadata { get; set; } + public virtual DbSet<Preference> Preferences { get; set; } + public virtual DbSet<ProviderMapping> ProviderMappings { get; set; } + public virtual DbSet<Rating> Ratings { get; set; } + + /// <summary> + /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to + /// store review ratings, not age ratings + /// </summary> + public virtual DbSet<RatingSource> RatingSources { get; set; } + public virtual DbSet<Release> Releases { get; set; } + public virtual DbSet<Season> Seasons { get; set; } + public virtual DbSet<SeasonMetadata> SeasonMetadata { get; set; } + public virtual DbSet<Series> Series { get; set; } + public virtual DbSet<SeriesMetadata> SeriesMetadata { get; set; } + public virtual DbSet<Track> Tracks { get; set; } + public virtual DbSet<TrackMetadata> TrackMetadata { get; set; } + public virtual DbSet<User> Users { get; set; } */ + + /// <summary> + /// Gets or sets the default connection string. + /// </summary> + public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db"; + + /// <inheritdoc /> + public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options) + { + } + + partial void CustomInit(DbContextOptionsBuilder optionsBuilder); + + /// <inheritdoc /> + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + CustomInit(optionsBuilder); + } + + partial void OnModelCreatingImpl(ModelBuilder modelBuilder); + partial void OnModelCreatedImpl(ModelBuilder modelBuilder); + + /// <inheritdoc /> + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + OnModelCreatingImpl(modelBuilder); + + modelBuilder.HasDefaultSchema("jellyfin"); + + /*modelBuilder.Entity<Artwork>().HasIndex(t => t.Kind); + + modelBuilder.Entity<Genre>().HasIndex(t => t.Name) + .IsUnique(); + + modelBuilder.Entity<LibraryItem>().HasIndex(t => t.UrlId) + .IsUnique();*/ + + OnModelCreatedImpl(modelBuilder); + } + + public override int SaveChanges() + { + foreach (var saveEntity in ChangeTracker.Entries() + .Where(e => e.State == EntityState.Modified) + .OfType<ISavingChanges>()) + { + saveEntity.OnSavingChanges(); + } + + return base.SaveChanges(); + } + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs new file mode 100644 index 0000000000..eab531d386 --- /dev/null +++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Server.Implementations +{ + /// <summary> + /// Factory class for generating new <see cref="JellyfinDb"/> instances. + /// </summary> + public class JellyfinDbProvider + { + private readonly IServiceProvider _serviceProvider; + + /// <summary> + /// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class. + /// </summary> + /// <param name="serviceProvider">The application's service provider.</param> + public JellyfinDbProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + serviceProvider.GetService<JellyfinDb>().Database.Migrate(); + } + + /// <summary> + /// Creates a new <see cref="JellyfinDb"/> context. + /// </summary> + /// <returns>The newly created context.</returns> + public JellyfinDb CreateContext() + { + return _serviceProvider.GetRequiredService<JellyfinDb>(); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs new file mode 100644 index 0000000000..98a83b7450 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs @@ -0,0 +1,72 @@ +#pragma warning disable CS1591 + +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20200514181226_AddActivityLog")] + partial class AddActivityLog + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<string>("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<string>("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs new file mode 100644 index 0000000000..5e0b454d8b --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs @@ -0,0 +1,46 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddActivityLog : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "jellyfin"); + + migrationBuilder.CreateTable( + name: "ActivityLogs", + schema: "jellyfin", + columns: table => new + { + Id = table.Column<int>(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column<string>(maxLength: 512, nullable: false), + Overview = table.Column<string>(maxLength: 512, nullable: true), + ShortOverview = table.Column<string>(maxLength: 512, nullable: true), + Type = table.Column<string>(maxLength: 256, nullable: false), + UserId = table.Column<Guid>(nullable: false), + ItemId = table.Column<string>(maxLength: 256, nullable: true), + DateCreated = table.Column<DateTime>(nullable: false), + LogSeverity = table.Column<int>(nullable: false), + RowVersion = table.Column<uint>(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ActivityLogs", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ActivityLogs", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs new file mode 100644 index 0000000000..72a4a8c3b6 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// <summary> + /// The design time factory for <see cref="JellyfinDb"/>. + /// This is only used for the creation of migrations and not during runtime. + /// </summary> + internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory<JellyfinDb> + { + public JellyfinDb CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder<JellyfinDb>(); + optionsBuilder.UseSqlite("Data Source=jellyfin.db"); + + return new JellyfinDb(optionsBuilder.Options); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs new file mode 100644 index 0000000000..1e7ffd2359 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -0,0 +1,66 @@ +// <auto-generated /> +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + partial class JellyfinDbModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<string>("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<string>("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 1d5313c13e..331a32c737 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,9 +1,18 @@ +using System; using System.Collections.Generic; +using System.IO; using System.Reflection; +using Emby.Drawing; using Emby.Server.Implementations; +using Jellyfin.Drawing.Skia; +using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Activity; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Jellyfin.Server @@ -20,27 +29,49 @@ namespace Jellyfin.Server /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> - /// <param name="imageEncoder">The <see cref="IImageEncoder" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param> public CoreAppHost( ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, StartupOptions options, IFileSystem fileSystem, - IImageEncoder imageEncoder, INetworkManager networkManager) : base( applicationPaths, loggerFactory, options, fileSystem, - imageEncoder, networkManager) { } - /// <inheritdoc /> - public override bool CanSelfRestart => StartupOptions.RestartPath != null; + /// <inheritdoc/> + protected override void RegisterServices(IServiceCollection serviceCollection) + { + // Register an image encoder + bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable(); + Type imageEncoderType = useSkiaEncoder + ? typeof(SkiaEncoder) + : typeof(NullImageEncoder); + serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType); + + // Log a warning if the Skia encoder could not be used + if (!useSkiaEncoder) + { + Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); + } + + // TODO: Set up scoping and use AddDbContextPool + serviceCollection.AddDbContext<JellyfinDb>( + options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), + ServiceLifetime.Transient); + + serviceCollection.AddSingleton<JellyfinDbProvider>(); + + serviceCollection.AddSingleton<IActivityManager, ActivityManager>(); + + base.RegisterServices(serviceCollection); + } /// <inheritdoc /> protected override void RestartInternal() => Program.Restart(); diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index dd4f9cd238..71ef9a69a2 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -71,6 +71,11 @@ namespace Jellyfin.Server.Extensions // Clear app parts to avoid other assemblies being picked up .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear()) .AddApplicationPart(typeof(StartupController).Assembly) + .AddJsonOptions(options => + { + // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. + options.JsonSerializerOptions.PropertyNamingPolicy = null; + }) .AddControllersAsServices(); } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 02ae202b47..c93aa837e7 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{07E39F42-A2C6-4B32-AF8C-725F957A73FF}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <AssemblyName>jellyfin</AssemblyName> <OutputType>Exe</OutputType> @@ -36,8 +41,10 @@ <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.7.82" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.4" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.4" /> + <PackageReference Include="prometheus-net" Version="3.5.0" /> + <PackageReference Include="prometheus-net.AspNetCore" Version="3.5.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> @@ -53,6 +60,7 @@ <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" /> <ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> <ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" /> + <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> </ItemGroup> </Project> diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs index eab995d67e..b79fdeac03 100644 --- a/Jellyfin.Server/Migrations/IMigrationRoutine.cs +++ b/Jellyfin.Server/Migrations/IMigrationRoutine.cs @@ -21,8 +21,6 @@ namespace Jellyfin.Server.Migrations /// <summary> /// Execute the migration routine. /// </summary> - /// <param name="host">Host that hosts current version.</param> - /// <param name="logger">Host logger.</param> - public void Perform(CoreAppHost host, ILogger logger); + public void Perform(); } } diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index b5ea04dcac..473f62737e 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using MediaBrowser.Common.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations @@ -13,10 +14,12 @@ namespace Jellyfin.Server.Migrations /// <summary> /// The list of known migrations, in order of applicability. /// </summary> - internal static readonly IMigrationRoutine[] Migrations = + private static readonly Type[] _migrationTypes = { - new Routines.DisableTranscodingThrottling(), - new Routines.CreateUserLoggingConfigFile() + typeof(Routines.DisableTranscodingThrottling), + typeof(Routines.CreateUserLoggingConfigFile), + typeof(Routines.MigrateActivityLogDb), + typeof(Routines.RemoveDuplicateExtras) }; /// <summary> @@ -27,6 +30,10 @@ namespace Jellyfin.Server.Migrations public static void Run(CoreAppHost host, ILoggerFactory loggerFactory) { var logger = loggerFactory.CreateLogger<MigrationRunner>(); + var migrations = _migrationTypes + .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m)) + .OfType<IMigrationRoutine>() + .ToArray(); var migrationOptions = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey); if (!host.ServerConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0) @@ -34,16 +41,16 @@ namespace Jellyfin.Server.Migrations // If startup wizard is not finished, this is a fresh install. // Don't run any migrations, just mark all of them as applied. logger.LogInformation("Marking all known migrations as applied because this is a fresh install"); - migrationOptions.Applied.AddRange(Migrations.Select(m => (m.Id, m.Name))); + migrationOptions.Applied.AddRange(migrations.Select(m => (m.Id, m.Name))); host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); return; } var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); - for (var i = 0; i < Migrations.Length; i++) + for (var i = 0; i < migrations.Length; i++) { - var migrationRoutine = Migrations[i]; + var migrationRoutine = migrations[i]; if (appliedMigrationIds.Contains(migrationRoutine.Id)) { logger.LogDebug("Skipping migration '{Name}' since it is already applied", migrationRoutine.Name); @@ -54,7 +61,7 @@ namespace Jellyfin.Server.Migrations try { - migrationRoutine.Perform(host, logger); + migrationRoutine.Perform(); } catch (Exception ex) { diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs index 3bc32c0478..89514c89b7 100644 --- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs +++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs @@ -36,6 +36,13 @@ namespace Jellyfin.Server.Migrations.Routines @"{""Serilog"":{""MinimumLevel"":""Information"",""WriteTo"":[{""Name"":""Console"",""Args"":{""outputTemplate"":""[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}""}},{""Name"":""Async"",""Args"":{""configure"":[{""Name"":""File"",""Args"":{""path"":""%JELLYFIN_LOG_DIR%//log_.log"",""rollingInterval"":""Day"",""retainedFileCountLimit"":3,""rollOnFileSizeLimit"":true,""fileSizeLimitBytes"":100000000,""outputTemplate"":""[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}:{Message}{NewLine}{Exception}""}}]}}],""Enrich"":[""FromLogContext"",""WithThreadId""]}}", }; + private readonly IApplicationPaths _appPaths; + + public CreateUserLoggingConfigFile(IApplicationPaths appPaths) + { + _appPaths = appPaths; + } + /// <inheritdoc/> public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}"); @@ -43,9 +50,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "CreateLoggingConfigHeirarchy"; /// <inheritdoc/> - public void Perform(CoreAppHost host, ILogger logger) + public void Perform() { - var logDirectory = host.Resolve<IApplicationPaths>().ConfigurationDirectoryPath; + var logDirectory = _appPaths.ConfigurationDirectoryPath; var existingConfigPath = Path.Combine(logDirectory, "logging.json"); // If the existing logging.json config file is unmodified, then 'reset' it by moving it to 'logging.old.json' diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs index 6f8e4a8ffb..b2e957d5bb 100644 --- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -10,6 +10,15 @@ namespace Jellyfin.Server.Migrations.Routines /// </summary> internal class DisableTranscodingThrottling : IMigrationRoutine { + private readonly ILogger _logger; + private readonly IConfigurationManager _configManager; + + public DisableTranscodingThrottling(ILogger<DisableTranscodingThrottling> logger, IConfigurationManager configManager) + { + _logger = logger; + _configManager = configManager; + } + /// <inheritdoc/> public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}"); @@ -17,16 +26,16 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "DisableTranscodingThrottling"; /// <inheritdoc/> - public void Perform(CoreAppHost host, ILogger logger) + public void Perform() { // Set EnableThrottling to false since it wasn't used before and may introduce issues - var encoding = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<EncodingOptions>("encoding"); + var encoding = _configManager.GetConfiguration<EncodingOptions>("encoding"); if (encoding.EnableThrottling) { - logger.LogInformation("Disabling transcoding throttling during migration"); + _logger.LogInformation("Disabling transcoding throttling during migration"); encoding.EnableThrottling = false; - host.ServerConfigurationManager.SaveConfiguration("encoding", encoding); + _configManager.SaveConfiguration("encoding", encoding); } } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs new file mode 100644 index 0000000000..b3cc297082 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Emby.Server.Implementations.Data; +using Jellyfin.Data.Entities; +using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// The migration routine for migrating the activity log database to EF Core. + /// </summary> + public class MigrateActivityLogDb : IMigrationRoutine + { + private const string DbFilename = "activitylog.db"; + + private readonly ILogger<MigrateActivityLogDb> _logger; + private readonly JellyfinDbProvider _provider; + private readonly IServerApplicationPaths _paths; + + /// <summary> + /// Initializes a new instance of the <see cref="MigrateActivityLogDb"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="paths">The server application paths.</param> + /// <param name="provider">The database provider.</param> + public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider) + { + _logger = logger; + _provider = provider; + _paths = paths; + } + + /// <inheritdoc/> + public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978"); + + /// <inheritdoc/> + public string Name => "MigrateActivityLogDatabase"; + + /// <inheritdoc/> + public void Perform() + { + var logLevelDictionary = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase) + { + { "None", LogLevel.None }, + { "Trace", LogLevel.Trace }, + { "Debug", LogLevel.Debug }, + { "Information", LogLevel.Information }, + { "Info", LogLevel.Information }, + { "Warn", LogLevel.Warning }, + { "Warning", LogLevel.Warning }, + { "Error", LogLevel.Error }, + { "Critical", LogLevel.Critical } + }; + + var dataPath = _paths.DataPath; + using (var connection = SQLite3.Open( + Path.Combine(dataPath, DbFilename), + ConnectionFlags.ReadOnly, + null)) + { + _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin."); + using var dbContext = _provider.CreateContext(); + + var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id ASC"); + + // Make sure that the database is empty in case of failed migration due to power outages, etc. + dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs); + dbContext.SaveChanges(); + // Reset the autoincrement counter + dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';"); + dbContext.SaveChanges(); + + var newEntries = queryResult.Select(entry => + { + if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity)) + { + severity = LogLevel.Trace; + } + + var newEntry = new ActivityLog( + entry[1].ToString(), + entry[4].ToString(), + entry[6].SQLiteType == SQLiteType.Null ? Guid.Empty : Guid.Parse(entry[6].ToString())) + { + DateCreated = entry[7].ReadDateTime(), + LogSeverity = severity + }; + + if (entry[2].SQLiteType != SQLiteType.Null) + { + newEntry.Overview = entry[2].ToString(); + } + + if (entry[3].SQLiteType != SQLiteType.Null) + { + newEntry.ShortOverview = entry[3].ToString(); + } + + if (entry[5].SQLiteType != SQLiteType.Null) + { + newEntry.ItemId = entry[5].ToString(); + } + + return newEntry; + }); + + dbContext.ActivityLogs.AddRange(newEntries); + dbContext.SaveChanges(); + } + + try + { + File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old")); + + var journalPath = Path.Combine(dataPath, DbFilename + "-journal"); + if (File.Exists(journalPath)) + { + File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal")); + } + } + catch (IOException e) + { + _logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'"); + } + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs new file mode 100644 index 0000000000..e955363881 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -0,0 +1,79 @@ +using System; +using System.Globalization; +using System.IO; + +using MediaBrowser.Controller; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself. + /// </summary> + internal class RemoveDuplicateExtras : IMigrationRoutine + { + private const string DbFilename = "library.db"; + private readonly ILogger _logger; + private readonly IServerApplicationPaths _paths; + + public RemoveDuplicateExtras(ILogger<RemoveDuplicateExtras> logger, IServerApplicationPaths paths) + { + _logger = logger; + _paths = paths; + } + + /// <inheritdoc/> + public Guid Id => Guid.Parse("{ACBE17B7-8435-4A83-8B64-6FCF162CB9BD}"); + + /// <inheritdoc/> + public string Name => "RemoveDuplicateExtras"; + + /// <inheritdoc/> + public void Perform() + { + var dataPath = _paths.DataPath; + var dbPath = Path.Combine(dataPath, DbFilename); + using (var connection = SQLite3.Open( + dbPath, + ConnectionFlags.ReadWrite, + null)) + { + // Query the database for the ids of duplicate extras + var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'"); + var bads = string.Join(", ", queryResult.SelectScalarString()); + + // Do nothing if no duplicate extras were detected + if (bads.Length == 0) + { + _logger.LogInformation("No duplicate extras detected, skipping migration."); + return; + } + + // Back up the database before deleting any entries + for (int i = 1; ; i++) + { + var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i); + if (!File.Exists(bakPath)) + { + try + { + File.Copy(dbPath, bakPath); + _logger.LogInformation("Library database backed up to {BackupPath}", bakPath); + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath); + throw; + } + } + } + + // Delete all duplicate extras + _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads); + connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')"); + } + } + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index e55b0d4ed9..b9895386f5 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -10,14 +10,11 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using CommandLine; -using Emby.Drawing; using Emby.Server.Implementations; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; -using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Extensions; using MediaBrowser.WebDashboard.Api; using Microsoft.AspNetCore.Hosting; @@ -161,30 +158,13 @@ namespace Jellyfin.Server ApplicationHost.LogEnvironmentInfo(_logger, appPaths); - // Make sure we have all the code pages we can get - // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - - // Increase the max http request limit - // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. - ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); - - // Disable the "Expect: 100-Continue" header by default - // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c - ServicePointManager.Expect100Continue = false; - - Batteries_V2.Init(); - if (raw.sqlite3_enable_shared_cache(1) != raw.SQLITE_OK) - { - _logger.LogWarning("Failed to enable shared cache for SQLite"); - } + PerformStaticInitialization(); var appHost = new CoreAppHost( appPaths, _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), - GetImageEncoder(appPaths), new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>())); try @@ -204,14 +184,13 @@ namespace Jellyfin.Server } ServiceCollection serviceCollection = new ServiceCollection(); - await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false); + appHost.Init(serviceCollection); - var webHost = CreateWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); + var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = webHost.Services; - appHost.InitializeServices(); - appHost.FindParts(); + await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); try @@ -252,14 +231,49 @@ namespace Jellyfin.Server } } - private static IWebHostBuilder CreateWebHostBuilder( + /// <summary> + /// Call static initialization methods for the application. + /// </summary> + public static void PerformStaticInitialization() + { + // Make sure we have all the code pages we can get + // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // Increase the max http request limit + // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. + ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); + + // Disable the "Expect: 100-Continue" header by default + // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c + ServicePointManager.Expect100Continue = false; + + Batteries_V2.Init(); + if (raw.sqlite3_enable_shared_cache(1) != raw.SQLITE_OK) + { + _logger.LogWarning("Failed to enable shared cache for SQLite"); + } + } + + /// <summary> + /// Configure the web host builder. + /// </summary> + /// <param name="builder">The builder to configure.</param> + /// <param name="appHost">The application host.</param> + /// <param name="serviceCollection">The application service collection.</param> + /// <param name="commandLineOpts">The command line options passed to the application.</param> + /// <param name="startupConfig">The application configuration.</param> + /// <param name="appPaths">The application paths.</param> + /// <returns>The configured web host builder.</returns> + public static IWebHostBuilder ConfigureWebHostBuilder( + this IWebHostBuilder builder, ApplicationHost appHost, IServiceCollection serviceCollection, StartupOptions commandLineOpts, IConfiguration startupConfig, IApplicationPaths appPaths) { - return new WebHostBuilder() + return builder .UseKestrel((builderContext, options) => { var addresses = appHost.ServerConfigurationManager @@ -267,15 +281,20 @@ namespace Jellyfin.Server .LocalNetworkAddresses .Select(appHost.NormalizeConfiguredLocalAddress) .Where(i => i != null) - .ToList(); - if (addresses.Any()) + .ToHashSet(); + if (addresses.Any() && !addresses.Contains(IPAddress.Any)) { + if (!addresses.Contains(IPAddress.Loopback)) + { + // we must listen on loopback for LiveTV to function regardless of the settings + addresses.Add(IPAddress.Loopback); + } + foreach (var address in addresses) { _logger.LogInformation("Kestrel listening on {IpAddress}", address); options.Listen(address, appHost.HttpPort); - - if (appHost.EnableHttps && appHost.Certificate != null) + if (appHost.ListenWithHttps) { options.Listen(address, appHost.HttpsPort, listenOptions => { @@ -285,11 +304,18 @@ namespace Jellyfin.Server } else if (builderContext.HostingEnvironment.IsDevelopment()) { - options.Listen(address, appHost.HttpsPort, listenOptions => + try { - listenOptions.UseHttps(); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); + options.Listen(address, appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + } + catch (InvalidOperationException ex) + { + _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); + } } } } @@ -298,7 +324,7 @@ namespace Jellyfin.Server _logger.LogInformation("Kestrel listening on all interfaces"); options.ListenAnyIP(appHost.HttpPort); - if (appHost.EnableHttps && appHost.Certificate != null) + if (appHost.ListenWithHttps) { options.ListenAnyIP(appHost.HttpsPort, listenOptions => { @@ -308,11 +334,18 @@ namespace Jellyfin.Server } else if (builderContext.HostingEnvironment.IsDevelopment()) { - options.ListenAnyIP(appHost.HttpsPort, listenOptions => + try { - listenOptions.UseHttps(); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); + options.ListenAnyIP(appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + } + catch (InvalidOperationException ex) + { + _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); + } } } }) @@ -492,7 +525,9 @@ namespace Jellyfin.Server /// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist /// already. /// </summary> - private static async Task InitLoggingConfigFile(IApplicationPaths appPaths) + /// <param name="appPaths">The application paths.</param> + /// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns> + public static async Task InitLoggingConfigFile(IApplicationPaths appPaths) { // Do nothing if the config file already exists string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, LoggingConfigFileDefault); @@ -512,7 +547,13 @@ namespace Jellyfin.Server await resource.CopyToAsync(dst).ConfigureAwait(false); } - private static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths) + /// <summary> + /// Create the application configuration. + /// </summary> + /// <param name="commandLineOpts">The command line options passed to the program.</param> + /// <param name="appPaths">The application paths.</param> + /// <returns>The application configuration.</returns> + public static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths) { return new ConfigurationBuilder() .ConfigureAppConfiguration(commandLineOpts, appPaths) @@ -571,25 +612,6 @@ namespace Jellyfin.Server } } - private static IImageEncoder GetImageEncoder(IApplicationPaths appPaths) - { - try - { - // Test if the native lib is available - SkiaEncoder.TestSkia(); - - return new SkiaEncoder( - _loggerFactory.CreateLogger<SkiaEncoder>(), - appPaths); - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); - } - - return new NullImageEncoder(); - } - private static void StartNewInstance(StartupOptions options) { _logger.LogInformation("Starting new instance"); diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 4d7d56e9d4..5f9a5c1618 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Prometheus; namespace Jellyfin.Server { @@ -63,15 +64,24 @@ namespace Jellyfin.Server app.UseResponseCompression(); // TODO app.UseMiddleware<WebSocketMiddleware>(); - app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync); // TODO use when old API is removed: app.UseAuthentication(); app.UseJellyfinApiSwagger(); app.UseRouting(); app.UseAuthorization(); + if (_serverConfigurationManager.Configuration.EnableMetrics) + { + // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad + app.UseHttpMetrics(); + } + app.UseEndpoints(endpoints => { endpoints.MapControllers(); + if (_serverConfigurationManager.Configuration.EnableMetrics) + { + endpoints.MapMetrics(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/metrics"); + } }); app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 6e15d058fc..cc250b06e2 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using CommandLine; using Emby.Server.Implementations; +using Emby.Server.Implementations.EntryPoints; +using Emby.Server.Implementations.Udp; using Emby.Server.Implementations.Updates; using MediaBrowser.Controller.Extensions; @@ -80,6 +83,10 @@ namespace Jellyfin.Server [Option("plugin-manifest-url", Required = false, HelpText = "A custom URL for the plugin repository JSON manifest")] public string? PluginManifestUrl { get; set; } + /// <inheritdoc /> + [Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")] + public Uri? PublishedServerUrl { get; set; } + /// <summary> /// Gets the command line options as a dictionary that can be used in the .NET configuration system. /// </summary> @@ -98,6 +105,11 @@ namespace Jellyfin.Server config.Add(ConfigurationExtensions.HostWebClientKey, bool.FalseString); } + if (PublishedServerUrl != null) + { + config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl.ToString()); + } + return config; } } diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 1a1d86362a..2cd68ac1b4 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Api public abstract class BaseApiService : IService, IRequiresRequest { public BaseApiService( - ILogger logger, + ILogger<BaseApiService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory) { @@ -34,7 +34,7 @@ namespace MediaBrowser.Api /// Gets the logger. /// </summary> /// <value>The logger.</value> - protected ILogger Logger { get; } + protected ILogger<BaseApiService> Logger { get; } /// <summary> /// Gets or sets the server configuration manager. diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs index 7004a2559e..53eb9667d8 100644 --- a/MediaBrowser.Api/Devices/DeviceService.cs +++ b/MediaBrowser.Api/Devices/DeviceService.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Net; @@ -116,11 +115,6 @@ namespace MediaBrowser.Api.Devices return _deviceManager.GetDeviceOptions(request.Id); } - public object Get(GetCameraUploads request) - { - return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId)); - } - public void Delete(DeleteDevice request) { var sessions = _authRepo.Get(new AuthenticationInfoQuery @@ -134,35 +128,5 @@ namespace MediaBrowser.Api.Devices _sessionManager.Logout(session); } } - - public Task Post(PostCameraUpload request) - { - var deviceId = Request.QueryString["DeviceId"]; - var album = Request.QueryString["Album"]; - var id = Request.QueryString["Id"]; - var name = Request.QueryString["Name"]; - var req = Request.Response.HttpContext.Request; - - if (req.HasFormContentType) - { - var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0]; - - return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo - { - MimeType = file.ContentType, - Album = album, - Name = name, - Id = id - }); - } - - return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo - { - MimeType = Request.ContentType, - Album = album, - Name = name, - Id = id - }); - } } } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index e8ea312519..2e9b3e6cb4 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -332,7 +332,8 @@ namespace MediaBrowser.Api.Images var fileInfo = _fileSystem.GetFileInfo(info.Path); length = fileInfo.Length; - ImageDimensions size = _imageProcessor.GetImageDimensions(item, info, true); + ImageDimensions size = _imageProcessor.GetImageDimensions(item, info); + _libraryManager.UpdateImages(item); width = size.Width; height = size.Height; @@ -606,6 +607,12 @@ namespace MediaBrowser.Api.Images IDictionary<string, string> headers, bool isHeadRequest) { + if (!image.IsLocalFile) + { + item ??= _libraryManager.GetItemById(itemId); + image = await _libraryManager.ConvertImageToLocal(item, image, request.Index ?? 0).ConfigureAwait(false); + } + var options = new ImageProcessingOptions { CropWhiteSpace = cropwhitespace, diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs index 222bb34d31..23bf547125 100644 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ b/MediaBrowser.Api/Images/RemoteImageService.cs @@ -265,17 +265,20 @@ namespace MediaBrowser.Api.Images { Url = url, BufferContent = false - }).ConfigureAwait(false); - var ext = result.ContentType.Split('/').Last(); + var ext = result.ContentType.Split('/')[^1]; var fullCachePath = GetFullCachePath(urlHash + "." + ext); Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); - using (var stream = result.Content) + var stream = result.Content; + await using (stream.ConfigureAwait(false)) { - using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); - await stream.CopyToAsync(filestream).ConfigureAwait(false); + var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + await using (filestream.ConfigureAwait(false)) + { + await stream.CopyToAsync(filestream).ConfigureAwait(false); + } } Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index 0bbe7e1cfa..68e3dfa59c 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -299,22 +299,26 @@ namespace MediaBrowser.Api { var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); - var ext = result.ContentType.Split('/').Last(); + var ext = result.ContentType.Split('/')[^1]; var fullCachePath = GetFullCachePath(urlHash + "." + ext); Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); - using (var stream = result.Content) + var stream = result.Content; + + await using (stream.ConfigureAwait(false)) { - using var fileStream = new FileStream( + var fileStream = new FileStream( fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); - - await stream.CopyToAsync(fileStream).ConfigureAwait(false); + await using (fileStream.ConfigureAwait(false)) + { + await stream.CopyToAsync(fileStream).ConfigureAwait(false); + } } Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index a54640b2fd..c0146dfee6 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -319,11 +319,14 @@ namespace MediaBrowser.Api.Library private readonly ILocalizationManager _localization; private readonly ILibraryMonitor _libraryMonitor; + private readonly ILogger<MoviesService> _moviesServiceLogger; + /// <summary> /// Initializes a new instance of the <see cref="LibraryService" /> class. /// </summary> public LibraryService( ILogger<LibraryService> logger, + ILogger<MoviesService> moviesServiceLogger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IProviderManager providerManager, @@ -344,6 +347,7 @@ namespace MediaBrowser.Api.Library _activityManager = activityManager; _localization = localization; _libraryMonitor = libraryMonitor; + _moviesServiceLogger = moviesServiceLogger; } private string[] GetRepresentativeItemTypes(string contentType) @@ -543,7 +547,7 @@ namespace MediaBrowser.Api.Library if (item is Movie || (program != null && program.IsMovie) || item is Trailer) { return new MoviesService( - Logger, + _moviesServiceLogger, ServerConfigurationManager, ResultFactory, _userManager, @@ -759,13 +763,12 @@ namespace MediaBrowser.Api.Library { try { - _activityManager.Create(new ActivityLogEntry + _activityManager.Create(new Jellyfin.Data.Entities.ActivityLog( + string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name), + "UserDownloadingContent", + auth.UserId) { - Name = string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name), - Type = "UserDownloadingContent", ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device), - UserId = auth.UserId - }); } catch diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 0d62cf8c59..d703bdb058 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</ProjectGuid> + </PropertyGroup> + <ItemGroup> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 46da8b9099..cdd0276340 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -82,7 +82,7 @@ namespace MediaBrowser.Api.Movies /// Initializes a new instance of the <see cref="MoviesService" /> class. /// </summary> public MoviesService( - ILogger logger, + ILogger<MoviesService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, diff --git a/MediaBrowser.Api/Movies/TrailersService.cs b/MediaBrowser.Api/Movies/TrailersService.cs index 8adf9c6216..0b53342359 100644 --- a/MediaBrowser.Api/Movies/TrailersService.cs +++ b/MediaBrowser.Api/Movies/TrailersService.cs @@ -33,13 +33,18 @@ namespace MediaBrowser.Api.Movies /// </summary> private readonly ILibraryManager _libraryManager; + /// <summary> + /// The logger for the created <see cref="ItemsService"/> instances. + /// </summary> + private readonly ILogger<ItemsService> _logger; + private readonly IDtoService _dtoService; private readonly ILocalizationManager _localizationManager; private readonly IJsonSerializer _json; private readonly IAuthorizationContext _authContext; public TrailersService( - ILogger<TrailersService> logger, + ILoggerFactory loggerFactory, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, @@ -48,7 +53,7 @@ namespace MediaBrowser.Api.Movies ILocalizationManager localizationManager, IJsonSerializer json, IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) + : base(loggerFactory.CreateLogger<TrailersService>(), serverConfigurationManager, httpResultFactory) { _userManager = userManager; _libraryManager = libraryManager; @@ -56,6 +61,7 @@ namespace MediaBrowser.Api.Movies _localizationManager = localizationManager; _json = json; _authContext = authContext; + _logger = loggerFactory.CreateLogger<ItemsService>(); } public object Get(Getrailers request) @@ -66,7 +72,7 @@ namespace MediaBrowser.Api.Movies getItems.IncludeItemTypes = "Trailer"; return new ItemsService( - Logger, + _logger, ServerConfigurationManager, ResultFactory, _userManager, diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index afc3e026a8..444354a992 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -42,23 +42,6 @@ namespace MediaBrowser.Api [Authenticated] public class GetPackages : IReturn<PackageInfo[]> { - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - [ApiMember(Name = "PackageType", Description = "Optional package type filter (System/UserInstalled)", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string PackageType { get; set; } - - [ApiMember(Name = "TargetSystems", Description = "Optional. Filter by target system type. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string TargetSystems { get; set; } - - [ApiMember(Name = "IsPremium", Description = "Optional. Filter by premium status", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? IsPremium { get; set; } - - [ApiMember(Name = "IsAdult", Description = "Optional. Filter by package that contain adult content.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? IsAdult { get; set; } - - public bool? IsAppStoreEnabled { get; set; } } /// <summary> @@ -88,13 +71,6 @@ namespace MediaBrowser.Api /// <value>The version.</value> [ApiMember(Name = "Version", Description = "Optional version. Defaults to latest version.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] public string Version { get; set; } - - /// <summary> - /// Gets or sets the update class. - /// </summary> - /// <value>The update class.</value> - [ApiMember(Name = "UpdateClass", Description = "Optional update class (Dev, Beta, Release). Defaults to Release.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public PackageVersionClass UpdateClass { get; set; } } /// <summary> @@ -154,23 +130,6 @@ namespace MediaBrowser.Api { IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); - if (!string.IsNullOrEmpty(request.TargetSystems)) - { - var apps = request.TargetSystems.Split(',').Select(i => (PackageTargetSystem)Enum.Parse(typeof(PackageTargetSystem), i, true)); - - packages = packages.Where(p => apps.Contains(p.targetSystem)); - } - - if (request.IsAdult.HasValue) - { - packages = packages.Where(p => p.adult == request.IsAdult.Value); - } - - if (request.IsAppStoreEnabled.HasValue) - { - packages = packages.Where(p => p.enableInAppStore == request.IsAppStoreEnabled.Value); - } - return ToOptimizedResult(packages.ToArray()); } @@ -186,8 +145,7 @@ namespace MediaBrowser.Api packages, request.Name, string.IsNullOrEmpty(request.AssemblyGuid) ? Guid.Empty : Guid.Parse(request.AssemblyGuid), - string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version), - request.UpdateClass).FirstOrDefault(); + string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version)).FirstOrDefault(); if (package == null) { diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index eb44cb4266..f796aa486d 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -81,7 +81,7 @@ namespace MediaBrowser.Api.Playback /// Initializes a new instance of the <see cref="BaseStreamingService" /> class. /// </summary> protected BaseStreamingService( - ILogger logger, + ILogger<BaseStreamingService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, @@ -134,7 +134,7 @@ namespace MediaBrowser.Api.Playback var data = $"{state.MediaPath}-{state.UserAgent}-{state.Request.DeviceId}-{state.Request.PlaySessionId}"; var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); - var ext = outputFileExtension.ToLowerInvariant(); + var ext = outputFileExtension?.ToLowerInvariant(); var folder = ServerConfigurationManager.GetTranscodePath(); return EnableOutputInSubFolder @@ -321,7 +321,7 @@ namespace MediaBrowser.Api.Playback var encodingOptions = ServerConfigurationManager.GetEncodingOptions(); // enable throttling when NOT using hardware acceleration - if (encodingOptions.HardwareAccelerationType == string.Empty) + if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) { return state.InputProtocol == MediaProtocol.File && state.RunTimeTicks.HasValue && @@ -330,6 +330,7 @@ namespace MediaBrowser.Api.Playback state.VideoType == VideoType.VideoFile && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase); } + return false; } diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 52962366c6..627421aac6 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Api.Playback.Hls public abstract class BaseHlsService : BaseStreamingService { public BaseHlsService( - ILogger logger, + ILogger<BaseHlsService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, @@ -209,24 +209,28 @@ namespace MediaBrowser.Api.Playback.Hls try { // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written - using var fileStream = GetPlaylistFileStream(playlist); - using var reader = new StreamReader(fileStream); - var count = 0; - - while (!reader.EndOfStream) + var fileStream = GetPlaylistFileStream(playlist); + await using (fileStream.ConfigureAwait(false)) { - var line = reader.ReadLine(); + using var reader = new StreamReader(fileStream); + var count = 0; - if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) + while (!reader.EndOfStream) { - count++; - if (count >= segmentCount) + var line = await reader.ReadLineAsync().ConfigureAwait(false); + + if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) { - Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist); - return; + count++; + if (count >= segmentCount) + { + Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist); + return; + } } } } + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } catch (IOException) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 20e18cc265..b8ea9c6da0 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -94,7 +94,7 @@ namespace MediaBrowser.Api.Playback.Hls public class DynamicHlsService : BaseHlsService { public DynamicHlsService( - ILogger logger, + ILogger<DynamicHlsService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, @@ -720,22 +720,203 @@ namespace MediaBrowser.Api.Playback.Hls //return state.VideoRequest.VideoBitRate.HasValue; } + /// <summary> + /// Get the H.26X level of the output video stream. + /// </summary> + /// <param name="state">StreamState of the current stream.</param> + /// <returns>H.26X level of the output video stream.</returns> + private int? GetOutputVideoCodecLevel(StreamState state) + { + string levelString; + if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) + && state.VideoStream.Level.HasValue) + { + levelString = state.VideoStream?.Level.ToString(); + } + else + { + levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec); + } + + if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) + { + return parsedLevel; + } + + return null; + } + + /// <summary> + /// Gets a formatted string of the output audio codec, for use in the CODECS field. + /// </summary> + /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/> + /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/> + /// <param name="state">StreamState of the current stream.</param> + /// <returns>Formatted audio codec string.</returns> + private string GetPlaylistAudioCodecs(StreamState state) + { + + if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + { + string profile = state.GetRequestedProfiles("aac").FirstOrDefault(); + + return HlsCodecStringFactory.GetAACString(profile); + } + else if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringFactory.GetMP3String(); + } + else if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringFactory.GetAC3String(); + } + else if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringFactory.GetEAC3String(); + } + + return string.Empty; + } + + /// <summary> + /// Gets a formatted string of the output video codec, for use in the CODECS field. + /// </summary> + /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/> + /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/> + /// <param name="state">StreamState of the current stream.</param> + /// <returns>Formatted video codec string.</returns> + private string GetPlaylistVideoCodecs(StreamState state, string codec, int level) + { + if (level == 0) + { + // This is 0 when there's no requested H.26X level in the device profile + // and the source is not encoded in H.26X + Logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); + return string.Empty; + } + + if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) + { + string profile = state.GetRequestedProfiles("h264").FirstOrDefault(); + + return HlsCodecStringFactory.GetH264String(profile, level); + } + else if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + string profile = state.GetRequestedProfiles("h265").FirstOrDefault(); + + return HlsCodecStringFactory.GetH265String(profile, level); + } + + return string.Empty; + } + + /// <summary> + /// Appends a CODECS field containing formatted strings of + /// the active streams output video and audio codecs. + /// </summary> + /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> + /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/> + /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/> + /// <param name="builder">StringBuilder to append the field to.</param> + /// <param name="state">StreamState of the current stream.</param> + private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state) + { + // Video + string videoCodecs = string.Empty; + int? videoCodecLevel = GetOutputVideoCodecLevel(state); + if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue) + { + videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value); + } + + // Audio + string audioCodecs = string.Empty; + if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) + { + audioCodecs = GetPlaylistAudioCodecs(state); + } + + StringBuilder codecs = new StringBuilder(); + + codecs.Append(videoCodecs) + .Append(',') + .Append(audioCodecs); + + if (codecs.Length > 1) + { + builder.Append(",CODECS=\"") + .Append(codecs) + .Append('"'); + } + } + + /// <summary> + /// Appends a FRAME-RATE field containing the framerate of the output stream. + /// </summary> + /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> + /// <param name="builder">StringBuilder to append the field to.</param> + /// <param name="state">StreamState of the current stream.</param> + private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state) + { + double? framerate = null; + if (state.TargetFramerate.HasValue) + { + framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3); + } + else if (state.VideoStream?.RealFrameRate != null) + { + framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3); + } + + if (framerate.HasValue) + { + builder.Append(",FRAME-RATE=\"") + .Append(framerate.Value) + .Append('"'); + } + } + + /// <summary> + /// Appends a RESOLUTION field containing the resolution of the output stream. + /// </summary> + /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> + /// <param name="builder">StringBuilder to append the field to.</param> + /// <param name="state">StreamState of the current stream.</param> + private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state) + { + if (state.OutputWidth.HasValue && state.OutputHeight.HasValue) + { + builder.Append(",RESOLUTION=\"") + .Append(state.OutputWidth.GetValueOrDefault()) + .Append('x') + .Append(state.OutputHeight.GetValueOrDefault()) + .Append('"'); + } + } + private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup) { - var header = "#EXT-X-STREAM-INF:BANDWIDTH=" + bitrate.ToString(CultureInfo.InvariantCulture) + ",AVERAGE-BANDWIDTH=" + bitrate.ToString(CultureInfo.InvariantCulture); + builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)) + .Append(",AVERAGE-BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)); - // tvos wants resolution, codecs, framerate - //if (state.TargetFramerate.HasValue) - //{ - // header += string.Format(",FRAME-RATE=\"{0}\"", state.TargetFramerate.Value.ToString(CultureInfo.InvariantCulture)); - //} + AppendPlaylistCodecsField(builder, state); + + AppendPlaylistResolutionField(builder, state); + + AppendPlaylistFramerateField(builder, state); if (!string.IsNullOrWhiteSpace(subtitleGroup)) { - header += string.Format(",SUBTITLES=\"{0}\"", subtitleGroup); + builder.Append(",SUBTITLES=\"") + .Append(subtitleGroup) + .Append('"'); } - builder.AppendLine(header); + builder.Append(Environment.NewLine); builder.AppendLine(url); } diff --git a/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs b/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs new file mode 100644 index 0000000000..3bbb77a65e --- /dev/null +++ b/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs @@ -0,0 +1,126 @@ +using System; +using System.Text; + + +namespace MediaBrowser.Api.Playback +{ + /// <summary> + /// Get various codec strings for use in HLS playlists. + /// </summary> + static class HlsCodecStringFactory + { + + /// <summary> + /// Gets a MP3 codec string. + /// </summary> + /// <returns>MP3 codec string.</returns> + public static string GetMP3String() + { + return "mp4a.40.34"; + } + + /// <summary> + /// Gets an AAC codec string. + /// </summary> + /// <param name="profile">AAC profile.</param> + /// <returns>AAC codec string.</returns> + public static string GetAACString(string profile) + { + StringBuilder result = new StringBuilder("mp4a", 9); + + if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".40.5"); + } + else + { + // Default to LC if profile is invalid + result.Append(".40.2"); + } + + return result.ToString(); + } + + /// <summary> + /// Gets a H.264 codec string. + /// </summary> + /// <param name="profile">H.264 profile.</param> + /// <param name="level">H.264 level.</param> + /// <returns>H.264 string.</returns> + public static string GetH264String(string profile, int level) + { + StringBuilder result = new StringBuilder("avc1", 11); + + if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".6400"); + } + else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".4D40"); + } + else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".42E0"); + } + else + { + // Default to constrained baseline if profile is invalid + result.Append(".4240"); + } + + string levelHex = level.ToString("X2"); + result.Append(levelHex); + + return result.ToString(); + } + + /// <summary> + /// Gets a H.265 codec string. + /// </summary> + /// <param name="profile">H.265 profile.</param> + /// <param name="level">H.265 level.</param> + /// <returns>H.265 string.</returns> + public static string GetH265String(string profile, int level) + { + // The h265 syntax is a bit of a mystery at the time this comment was written. + // This is what I've found through various sources: + // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN] + StringBuilder result = new StringBuilder("hev1", 16); + + if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)) + { + result.Append(".2.6"); + } + else + { + // Default to main if profile is invalid + result.Append(".1.6"); + } + + result.Append(".L") + .Append(level * 3) + .Append(".B0"); + + return result.ToString(); + } + + /// <summary> + /// Gets an AC-3 codec string. + /// </summary> + /// <returns>AC-3 codec string.</returns> + public static string GetAC3String() + { + return "mp4a.a5"; + } + + /// <summary> + /// Gets an E-AC-3 codec string. + /// </summary> + /// <returns>E-AC-3 codec string.</returns> + public static string GetEAC3String() + { + return "mp4a.a6"; + } + } +} diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index 3638feaa33..e2d771ec65 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -79,7 +79,7 @@ namespace MediaBrowser.Api.Playback private readonly IAuthorizationContext _authContext; public MediaInfoService( - ILogger logger, + ILogger<MediaInfoService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IMediaSourceManager mediaSourceManager, @@ -520,10 +520,7 @@ namespace MediaBrowser.Api.Playback streamInfo.StartPositionTicks = startTimeTicks; mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 8d1e3a3f23..34c7986ca5 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Api.Playback.Progressive public class AudioService : BaseProgressiveStreamingService { public AudioService( - ILogger logger, + ILogger<AudioService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IHttpClient httpClient, diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index ed68219c9f..c7bf055fba 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Api.Playback.Progressive protected IHttpClient HttpClient { get; private set; } public BaseProgressiveStreamingService( - ILogger logger, + ILogger<BaseProgressiveStreamingService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IHttpClient httpClient, diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs index cebd4b49a1..a3b319d44b 100644 --- a/MediaBrowser.Api/Playback/UniversalAudioService.cs +++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs @@ -75,9 +75,11 @@ namespace MediaBrowser.Api.Playback public class UniversalAudioService : BaseApiService { private readonly EncodingHelper _encodingHelper; + private readonly ILoggerFactory _loggerFactory; public UniversalAudioService( ILogger<UniversalAudioService> logger, + ILoggerFactory loggerFactory, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IHttpClient httpClient, @@ -108,6 +110,7 @@ namespace MediaBrowser.Api.Playback AuthorizationContext = authorizationContext; NetworkManager = networkManager; _encodingHelper = encodingHelper; + _loggerFactory = loggerFactory; } protected IHttpClient HttpClient { get; private set; } @@ -233,7 +236,7 @@ namespace MediaBrowser.Api.Playback AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId; var mediaInfoService = new MediaInfoService( - Logger, + _loggerFactory.CreateLogger<MediaInfoService>(), ServerConfigurationManager, ResultFactory, MediaSourceManager, @@ -277,7 +280,7 @@ namespace MediaBrowser.Api.Playback if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { var service = new DynamicHlsService( - Logger, + _loggerFactory.CreateLogger<DynamicHlsService>(), ServerConfigurationManager, ResultFactory, UserManager, @@ -331,7 +334,7 @@ namespace MediaBrowser.Api.Playback else { var service = new AudioService( - Logger, + _loggerFactory.CreateLogger<AudioService>(), ServerConfigurationManager, ResultFactory, HttpClient, diff --git a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs index d882aac887..0e74c92679 100644 --- a/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs +++ b/MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs @@ -31,46 +31,46 @@ namespace MediaBrowser.Api.Sessions { _sessionManager = sessionManager; - _sessionManager.SessionStarted += _sessionManager_SessionStarted; - _sessionManager.SessionEnded += _sessionManager_SessionEnded; - _sessionManager.PlaybackStart += _sessionManager_PlaybackStart; - _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped; - _sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress; - _sessionManager.CapabilitiesChanged += _sessionManager_CapabilitiesChanged; - _sessionManager.SessionActivity += _sessionManager_SessionActivity; + _sessionManager.SessionStarted += OnSessionManagerSessionStarted; + _sessionManager.SessionEnded += OnSessionManagerSessionEnded; + _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart; + _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped; + _sessionManager.PlaybackProgress += OnSessionManagerPlaybackProgress; + _sessionManager.CapabilitiesChanged += OnSessionManagerCapabilitiesChanged; + _sessionManager.SessionActivity += OnSessionManagerSessionActivity; } - void _sessionManager_SessionActivity(object sender, SessionEventArgs e) + private void OnSessionManagerSessionActivity(object sender, SessionEventArgs e) { SendData(false); } - void _sessionManager_CapabilitiesChanged(object sender, SessionEventArgs e) + private void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e) { SendData(true); } - void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e) + private void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e) { SendData(!e.IsAutomated); } - void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e) + private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e) { SendData(true); } - void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e) + private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e) { SendData(true); } - void _sessionManager_SessionEnded(object sender, SessionEventArgs e) + private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e) { SendData(true); } - void _sessionManager_SessionStarted(object sender, SessionEventArgs e) + private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e) { SendData(true); } @@ -84,15 +84,16 @@ namespace MediaBrowser.Api.Sessions return Task.FromResult(_sessionManager.Sessions); } + /// <inheritdoc /> protected override void Dispose(bool dispose) { - _sessionManager.SessionStarted -= _sessionManager_SessionStarted; - _sessionManager.SessionEnded -= _sessionManager_SessionEnded; - _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart; - _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped; - _sessionManager.PlaybackProgress -= _sessionManager_PlaybackProgress; - _sessionManager.CapabilitiesChanged -= _sessionManager_CapabilitiesChanged; - _sessionManager.SessionActivity -= _sessionManager_SessionActivity; + _sessionManager.SessionStarted -= OnSessionManagerSessionStarted; + _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; + _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart; + _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; + _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress; + _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged; + _sessionManager.SessionActivity -= OnSessionManagerSessionActivity; base.Dispose(dispose); } diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs new file mode 100644 index 0000000000..1e14ea552c --- /dev/null +++ b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs @@ -0,0 +1,302 @@ +using System.Threading; +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.SyncPlay +{ + [Route("/SyncPlay/{SessionId}/NewGroup", "POST", Summary = "Create a new SyncPlay group")] + [Authenticated] + public class SyncPlayNewGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing SyncPlay group")] + [Authenticated] + public class SyncPlayJoinGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// <summary> + /// Gets or sets the Group id. + /// </summary> + /// <value>The Group id to join.</value> + [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string GroupId { get; set; } + + /// <summary> + /// Gets or sets the playing item id. + /// </summary> + /// <value>The client's currently playing item id.</value> + [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlayingItemId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")] + [Authenticated] + public class SyncPlayLeaveGroup : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/ListGroups", "POST", Summary = "List SyncPlay groups")] + [Authenticated] + public class SyncPlayListGroups : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// <summary> + /// Gets or sets the filter item id. + /// </summary> + /// <value>The filter item id.</value> + [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string FilterItemId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/PlayRequest", "POST", Summary = "Request play in SyncPlay group")] + [Authenticated] + public class SyncPlayPlayRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in SyncPlay group")] + [Authenticated] + public class SyncPlayPauseRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + } + + [Route("/SyncPlay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in SyncPlay group")] + [Authenticated] + public class SyncPlaySeekRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] + public long PositionTicks { get; set; } + } + + [Route("/SyncPlay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")] + [Authenticated] + public class SyncPlayBufferingRequest : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + /// <summary> + /// Gets or sets the date used to pin PositionTicks in time. + /// </summary> + /// <value>The date related to PositionTicks.</value> + [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string When { get; set; } + + [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] + public long PositionTicks { get; set; } + + /// <summary> + /// Gets or sets whether this is a buffering or a buffering-done request. + /// </summary> + /// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value> + [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] + public bool BufferingDone { get; set; } + } + + [Route("/SyncPlay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")] + [Authenticated] + public class SyncPlayUpdatePing : IReturnVoid + { + [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string SessionId { get; set; } + + [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")] + public double Ping { get; set; } + } + + /// <summary> + /// Class SyncPlayService. + /// </summary> + public class SyncPlayService : BaseApiService + { + /// <summary> + /// The session context. + /// </summary> + private readonly ISessionContext _sessionContext; + + /// <summary> + /// The SyncPlay manager. + /// </summary> + private readonly ISyncPlayManager _syncPlayManager; + + public SyncPlayService( + ILogger<SyncPlayService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + ISessionContext sessionContext, + ISyncPlayManager syncPlayManager) + : base(logger, serverConfigurationManager, httpResultFactory) + { + _sessionContext = sessionContext; + _syncPlayManager = syncPlayManager; + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(SyncPlayNewGroup request) + { + var currentSession = GetSession(_sessionContext); + _syncPlayManager.NewGroup(currentSession, CancellationToken.None); + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(SyncPlayJoinGroup request) + { + var currentSession = GetSession(_sessionContext); + + Guid groupId; + Guid playingItemId = Guid.Empty; + + if (!Guid.TryParse(request.GroupId, out groupId)) + { + Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId); + return; + } + + // Both null and empty strings mean that client isn't playing anything + if (!string.IsNullOrEmpty(request.PlayingItemId) && !Guid.TryParse(request.PlayingItemId, out playingItemId)) + { + Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId); + return; + } + + var joinRequest = new JoinGroupRequest() + { + GroupId = groupId, + PlayingItemId = playingItemId + }; + + _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None); + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(SyncPlayLeaveGroup request) + { + var currentSession = GetSession(_sessionContext); + _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <value>The requested list of groups.</value> + public List<GroupInfoView> Post(SyncPlayListGroups request) + { + var currentSession = GetSession(_sessionContext); + var filterItemId = Guid.Empty; + + if (!string.IsNullOrEmpty(request.FilterItemId) && !Guid.TryParse(request.FilterItemId, out filterItemId)) + { + Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId); + } + + return _syncPlayManager.ListGroups(currentSession, filterItemId); + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(SyncPlayPlayRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Play + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(SyncPlayPauseRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Pause + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(SyncPlaySeekRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Seek, + PositionTicks = request.PositionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(SyncPlayBufferingRequest request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, + When = DateTime.Parse(request.When), + PositionTicks = request.PositionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + public void Post(SyncPlayUpdatePing request) + { + var currentSession = GetSession(_sessionContext); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.UpdatePing, + Ping = Convert.ToInt64(request.Ping) + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + } + } +} diff --git a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs new file mode 100644 index 0000000000..4a9307e62f --- /dev/null +++ b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs @@ -0,0 +1,52 @@ +using System; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api.SyncPlay +{ + [Route("/GetUtcTime", "GET", Summary = "Get UtcTime")] + public class GetUtcTime : IReturnVoid + { + // Nothing + } + + /// <summary> + /// Class TimeSyncService. + /// </summary> + public class TimeSyncService : BaseApiService + { + public TimeSyncService( + ILogger<TimeSyncService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory) + : base(logger, serverConfigurationManager, httpResultFactory) + { + // Do nothing + } + + /// <summary> + /// Handles the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <value>The current UTC time response.</value> + public UtcTimeResponse Get(GetUtcTime request) + { + // Important to keep the following line at the beginning + var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); + + var response = new UtcTimeResponse(); + response.RequestReceptionTime = requestReceptionTime; + + // Important to keep the following two lines at the end + var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); + response.ResponseTransmissionTime = responseTransmissionTime; + + // Implementing NTP on such a high level results in this useless + // information being sent. On the other hand it enables future additions. + return response; + } + } +} diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs index f95fa7ca0b..a6bacad4fc 100644 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ b/MediaBrowser.Api/System/ActivityLogService.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; +using System.Linq; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; @@ -53,7 +55,10 @@ namespace MediaBrowser.Api.System (DateTime?)null : DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - var result = _activityManager.GetActivityLogEntries(minDate, request.HasUserId, request.StartIndex, request.Limit); + var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>( + entries => entries.Where(entry => entry.DateCreated >= minDate)); + + var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit); return ToOptimizedResult(result); } diff --git a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs index f8b6ee65d6..8e4860be4b 100644 --- a/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs +++ b/MediaBrowser.Api/System/ActivityLogWebSocketListener.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; using System.Threading.Tasks; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; @@ -10,7 +10,7 @@ namespace MediaBrowser.Api.System /// <summary> /// Class SessionInfoWebSocketListener /// </summary> - public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState> + public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState> { /// <summary> /// Gets the name. @@ -26,10 +26,10 @@ namespace MediaBrowser.Api.System public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger) { _activityManager = activityManager; - _activityManager.EntryCreated += _activityManager_EntryCreated; + _activityManager.EntryCreated += OnEntryCreated; } - void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e) + private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e) { SendData(true); } @@ -38,15 +38,15 @@ namespace MediaBrowser.Api.System /// Gets the data to send. /// </summary> /// <returns>Task{SystemInfo}.</returns> - protected override Task<List<ActivityLogEntry>> GetDataToSend() + protected override Task<ActivityLogEntry[]> GetDataToSend() { - return Task.FromResult(new List<ActivityLogEntry>()); + return Task.FromResult(Array.Empty<ActivityLogEntry>()); } - + /// <inheritdoc /> protected override void Dispose(bool dispose) { - _activityManager.EntryCreated -= _activityManager_EntryCreated; + _activityManager.EntryCreated -= OnEntryCreated; base.Dispose(dispose); } diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs index 3d08d5437c..bef91d54df 100644 --- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs +++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs @@ -51,7 +51,7 @@ namespace MediaBrowser.Api.UserLibrary public class ArtistsService : BaseItemsByNameService<MusicArtist> { public ArtistsService( - ILogger<GenresService> logger, + ILogger<ArtistsService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index c4a52d5f52..559082ff48 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Api.UserLibrary /// <param name="userDataRepository">The user data repository.</param> /// <param name="dtoService">The dto service.</param> protected BaseItemsByNameService( - ILogger logger, + ILogger<BaseItemsByNameService<TItemType>> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index ac59c30301..f3c0441e12 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.Api.UserLibrary /// <param name="localization">The localization.</param> /// <param name="dtoService">The dto service.</param> public ItemsService( - ILogger logger, + ILogger<ItemsService> logger, IServerConfigurationManager serverConfigurationManager, IHttpResultFactory httpResultFactory, IUserManager userManager, @@ -242,11 +242,11 @@ namespace MediaBrowser.Api.UserLibrary return folder.GetItems(GetItemsQuery(request, dtoOptions, user)); } - var itemsArray = folder.GetChildren(user, true).ToArray(); + var itemsArray = folder.GetChildren(user, true); return new QueryResult<BaseItem> { Items = itemsArray, - TotalRecordCount = itemsArray.Length, + TotalRecordCount = itemsArray.Count, StartIndex = 0 }; } diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index ebcacd2a3d..98eeda5440 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -437,7 +437,7 @@ namespace MediaBrowser.Api catch (SecurityException e) { // rethrow adding IP address to message - throw new SecurityException($"[{Request.RemoteIp}] {e.Message}"); + throw new SecurityException($"[{Request.RemoteIp}] {e.Message}", e); } } diff --git a/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs b/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs index ccf9658988..89740ae084 100644 --- a/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs +++ b/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using MediaBrowser.Model.Configuration; @@ -17,18 +18,25 @@ namespace MediaBrowser.Common.Configuration => configurationManager.GetConfiguration<EncodingOptions>("encoding"); /// <summary> - /// Retrieves the transcoding temp path from the encoding configuration. + /// Retrieves the transcoding temp path from the encoding configuration, falling back to a default if no path + /// is specified in configuration. If the directory does not exist, it will be created. /// </summary> - /// <param name="configurationManager">The Configuration manager.</param> + /// <param name="configurationManager">The configuration manager.</param> /// <returns>The transcoding temp path.</returns> + /// <exception cref="UnauthorizedAccessException">If the directory does not exist, and the caller does not have the required permission to create it.</exception> + /// <exception cref="NotSupportedException">If there is a custom path transcoding path specified, but it is invalid.</exception> + /// <exception cref="IOException">If the directory does not exist, and it also could not be created.</exception> public static string GetTranscodePath(this IConfigurationManager configurationManager) { + // Get the configured path and fall back to a default var transcodingTempPath = configurationManager.GetEncodingOptions().TranscodingTempPath; if (string.IsNullOrEmpty(transcodingTempPath)) { - return Path.Combine(configurationManager.CommonApplicationPaths.ProgramDataPath, "transcodes"); + transcodingTempPath = Path.Combine(configurationManager.CommonApplicationPaths.ProgramDataPath, "transcodes"); } + // Make sure the directory exists + Directory.CreateDirectory(transcodingTempPath); return transcodingTempPath; } } diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index 08964420e7..40020093b6 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Security.Cryptography; using System.Text; diff --git a/MediaBrowser.Common/Extensions/CopyToExtensions.cs b/MediaBrowser.Common/Extensions/CopyToExtensions.cs index 2ecbc6539b..94bf7c7401 100644 --- a/MediaBrowser.Common/Extensions/CopyToExtensions.cs +++ b/MediaBrowser.Common/Extensions/CopyToExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + using System.Collections.Generic; namespace MediaBrowser.Common.Extensions diff --git a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs index 48e758ee4c..258bd6662c 100644 --- a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs +++ b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; namespace MediaBrowser.Common.Extensions diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs index c747871222..2f52ba196a 100644 --- a/MediaBrowser.Common/Extensions/ProcessExtensions.cs +++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Diagnostics; using System.Threading; diff --git a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs index 95802a4626..7c7bdaa92f 100644 --- a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs +++ b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs @@ -1,3 +1,4 @@ +#nullable enable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs index 22130c5a1e..ebac9d8e6b 100644 --- a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs +++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; namespace MediaBrowser.Common.Extensions diff --git a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs index 0432f36b57..459bec1105 100644 --- a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs +++ b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; diff --git a/MediaBrowser.Common/Extensions/StringExtensions.cs b/MediaBrowser.Common/Extensions/StringExtensions.cs new file mode 100644 index 0000000000..7643017412 --- /dev/null +++ b/MediaBrowser.Common/Extensions/StringExtensions.cs @@ -0,0 +1,37 @@ +#nullable enable + +using System; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Extensions methods to simplify string operations. + /// </summary> + public static class StringExtensions + { + /// <summary> + /// Returns the part on the left of the <c>needle</c>. + /// </summary> + /// <param name="haystack">The string to seek.</param> + /// <param name="needle">The needle to find.</param> + /// <returns>The part left of the <paramref name="needle" />.</returns> + public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, char needle) + { + var pos = haystack.IndexOf(needle); + return pos == -1 ? haystack : haystack[..pos]; + } + + /// <summary> + /// Returns the part on the left of the <c>needle</c>. + /// </summary> + /// <param name="haystack">The string to seek.</param> + /// <param name="needle">The needle to find.</param> + /// <param name="stringComparison">One of the enumeration values that specifies the rules for the search.</param> + /// <returns>The part left of the <c>needle</c>.</returns> + public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle, StringComparison stringComparison = default) + { + var pos = haystack.IndexOf(needle, stringComparison); + return pos == -1 ? haystack : haystack[..pos]; + } + } +} diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 0e282cf538..e8d9282e40 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Updates; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common @@ -48,12 +46,6 @@ namespace MediaBrowser.Common /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value> bool CanSelfRestart { get; } - /// <summary> - /// Gets the version class of the system. - /// </summary> - /// <value><see cref="PackageVersionClass.Release" /> or <see cref="PackageVersionClass.Beta" />.</value> - PackageVersionClass SystemUpdateLevel { get; } - /// <summary> /// Gets the application version. /// </summary> @@ -125,9 +117,7 @@ namespace MediaBrowser.Common /// Initializes this instance. /// </summary> /// <param name="serviceCollection">The service collection.</param> - /// <param name="startupConfig">The configuration to use for initialization.</param> - /// <returns>A task.</returns> - Task InitAsync(IServiceCollection serviceCollection, IConfiguration startupConfig); + void Init(IServiceCollection serviceCollection); /// <summary> /// Creates the instance. diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 3b03478020..a597b90524 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{9142EEFA-7570-41E1-BFCC-468BB571AF2F}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Common</PackageId> @@ -12,8 +17,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.4" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> </ItemGroup> diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index b24d10ff10..9e4a360c38 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -67,7 +67,7 @@ namespace MediaBrowser.Common.Plugins } /// <summary> - /// Called when just before the plugin is uninstalled from the server. + /// Called just before the plugin is uninstalled from the server. /// </summary> public virtual void OnUninstalling() { @@ -101,7 +101,7 @@ namespace MediaBrowser.Common.Plugins private readonly object _configurationSyncLock = new object(); /// <summary> - /// The save lock. + /// The configuration save lock. /// </summary> private readonly object _configurationSaveLock = new object(); @@ -148,7 +148,7 @@ namespace MediaBrowser.Common.Plugins protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath); /// <summary> - /// Gets or sets the plugin's configuration. + /// Gets or sets the plugin configuration. /// </summary> /// <value>The configuration.</value> public TConfigurationType Configuration @@ -186,7 +186,7 @@ namespace MediaBrowser.Common.Plugins public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName); /// <summary> - /// Gets the plugin's configuration. + /// Gets the plugin configuration. /// </summary> /// <value>The configuration.</value> BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration; diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index 93f330e5b8..950604432d 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -28,12 +28,12 @@ namespace MediaBrowser.Common.Updates /// <summary> /// Occurs when a plugin is updated. /// </summary> - event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated; + event EventHandler<GenericEventArgs<(IPlugin, VersionInfo)>> PluginUpdated; /// <summary> /// Occurs when a plugin is installed. /// </summary> - event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled; + event EventHandler<GenericEventArgs<VersionInfo>> PluginInstalled; /// <summary> /// Gets the completed installations. @@ -64,12 +64,10 @@ namespace MediaBrowser.Common.Updates /// </summary> /// <param name="availableVersions">The available version of the plugin.</param> /// <param name="minVersion">The minimum required version of the plugin.</param> - /// <param name="classification">The classification of updates.</param> /// <returns>All compatible versions ordered from newest to oldest.</returns> - IEnumerable<PackageVersionInfo> GetCompatibleVersions( - IEnumerable<PackageVersionInfo> availableVersions, - Version minVersion = null, - PackageVersionClass classification = PackageVersionClass.Release); + IEnumerable<VersionInfo> GetCompatibleVersions( + IEnumerable<VersionInfo> availableVersions, + Version minVersion = null); /// <summary> /// Returns all compatible versions ordered from newest to oldest. @@ -78,21 +76,19 @@ namespace MediaBrowser.Common.Updates /// <param name="name">The name.</param> /// <param name="guid">The guid of the plugin.</param> /// <param name="minVersion">The minimum required version of the plugin.</param> - /// <param name="classification">The classification.</param> /// <returns>All compatible versions ordered from newest to oldest.</returns> - IEnumerable<PackageVersionInfo> GetCompatibleVersions( + IEnumerable<VersionInfo> GetCompatibleVersions( IEnumerable<PackageInfo> availablePackages, string name = null, Guid guid = default, - Version minVersion = null, - PackageVersionClass classification = PackageVersionClass.Release); + Version minVersion = null); /// <summary> /// Returns the available plugin updates. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The available plugin updates.</returns> - Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default); + Task<IEnumerable<VersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default); /// <summary> /// Installs the package. @@ -100,7 +96,7 @@ namespace MediaBrowser.Common.Updates /// <param name="package">The package.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns><see cref="Task" />.</returns> - Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken = default); + Task InstallPackage(VersionInfo package, CancellationToken cancellationToken = default); /// <summary> /// Uninstalls a plugin. diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs index 36e124ddfc..11eb2ad34a 100644 --- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs @@ -8,6 +8,6 @@ namespace MediaBrowser.Common.Updates { public InstallationInfo InstallationInfo { get; set; } - public PackageVersionInfo PackageVersionInfo { get; set; } + public VersionInfo VersionInfo { get; set; } } } diff --git a/MediaBrowser.Controller/Authentication/AuthenticationException.cs b/MediaBrowser.Controller/Authentication/AuthenticationException.cs index 62eca3ea9f..081f877f72 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationException.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationException.cs @@ -7,23 +7,29 @@ namespace MediaBrowser.Controller.Authentication /// </summary> public class AuthenticationException : Exception { - /// <inheritdoc /> + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> public AuthenticationException() : base() { - } - /// <inheritdoc /> + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> public AuthenticationException(string message) : base(message) { - } - /// <inheritdoc /> + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> + /// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param> public AuthenticationException(string message, Exception innerException) : base(message, innerException) { - } } } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 77d5676310..ef3f43c759 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,6 +1,4 @@ using System; -using System.IO; -using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Events; @@ -11,11 +9,6 @@ namespace MediaBrowser.Controller.Devices { public interface IDeviceManager { - /// <summary> - /// Occurs when [camera image uploaded]. - /// </summary> - event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; - /// <summary> /// Saves the capabilities. /// </summary> @@ -45,22 +38,6 @@ namespace MediaBrowser.Controller.Devices /// <returns>IEnumerable<DeviceInfo>.</returns> QueryResult<DeviceInfo> GetDevices(DeviceQuery query); - /// <summary> - /// Gets the upload history. - /// </summary> - /// <param name="deviceId">The device identifier.</param> - /// <returns>ContentUploadHistory.</returns> - ContentUploadHistory GetCameraUploadHistory(string deviceId); - - /// <summary> - /// Accepts the upload. - /// </summary> - /// <param name="deviceId">The device identifier.</param> - /// <param name="stream">The stream.</param> - /// <param name="file">The file.</param> - /// <returns>Task.</returns> - Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file); - /// <summary> /// Determines whether this instance [can access device] the specified user identifier. /// </summary> diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 79399807fd..36c746624e 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -40,15 +40,6 @@ namespace MediaBrowser.Controller.Drawing /// <returns>ImageDimensions</returns> ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info); - /// <summary> - /// Gets the dimensions of the image. - /// </summary> - /// <param name="item">The base item.</param> - /// <param name="info">The information.</param> - /// <param name="updateItem">Whether or not the item info should be updated.</param> - /// <returns>ImageDimensions</returns> - ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem); - /// <summary> /// Gets the image cache tag. /// </summary> diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 56a361e0ec..7ed8fa7671 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -549,7 +549,6 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public DateTime DateModified { get; set; } - [JsonIgnore] public DateTime DateLastSaved { get; set; } [JsonIgnore] @@ -2741,7 +2740,7 @@ namespace MediaBrowser.Controller.Entities { var list = GetEtagValues(user); - return string.Join("|", list.ToArray()).GetMD5().ToString("N", CultureInfo.InvariantCulture); + return string.Join("|", list).GetMD5().ToString("N", CultureInfo.InvariantCulture); } protected virtual List<string> GetEtagValues(User user) @@ -2784,8 +2783,7 @@ namespace MediaBrowser.Controller.Entities return true; } - var view = this as IHasCollectionType; - if (view != null) + if (this is IHasCollectionType view) { if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index bb48605e55..a468e0c35a 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -864,7 +864,7 @@ namespace MediaBrowser.Controller.Entities return SortItemsByRequest(query, result); } - return result.ToArray(); + return result; } return GetItemsInternal(query).Items; diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 608ffc61c2..d1d6c74b86 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -39,10 +39,9 @@ namespace MediaBrowser.Controller int HttpsPort { get; } /// <summary> - /// Gets a value indicating whether [supports HTTPS]. + /// Gets a value indicating whether the server should listen on an HTTPS port. /// </summary> - /// <value><c>true</c> if [supports HTTPS]; otherwise, <c>false</c>.</value> - bool EnableHttps { get; } + bool ListenWithHttps { get; } /// <summary> /// Gets a value indicating whether this instance has update available. @@ -57,31 +56,51 @@ namespace MediaBrowser.Controller string FriendlyName { get; } /// <summary> - /// Gets the local ip address. + /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request + /// to the API that should exist at the address. /// </summary> - /// <value>The local ip address.</value> + /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param> + /// <returns>A list containing all the local IP addresses of the server.</returns> Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken); /// <summary> - /// Gets the local API URL. + /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured + /// IP address that can be found via <see cref="GetLocalIpAddresses"/>. HTTPS will be preferred when available. /// </summary> - /// <value>The local API URL.</value> + /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param> + /// <returns>The server URL.</returns> Task<string> GetLocalApiUrl(CancellationToken cancellationToken); /// <summary> - /// Gets the local API URL. + /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1) + /// over HTTP (not HTTPS). /// </summary> - /// <param name="hostname">The hostname.</param> - /// <returns>The local API URL.</returns> - string GetLocalApiUrl(ReadOnlySpan<char> hostname); + /// <returns>The API URL.</returns> + string GetLoopbackHttpApiUrl(); /// <summary> - /// Gets the local API URL. + /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available. /// </summary> - /// <param name="address">The IP address.</param> - /// <returns>The local API URL.</returns> + /// <param name="address">The IP address to use as the hostname in the URL.</param> + /// <returns>The API URL.</returns> string GetLocalApiUrl(IPAddress address); + /// <summary> + /// Gets a local (LAN) URL that can be used to access the API. + /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair. + /// </summary> + /// <param name="hostname">The hostname to use in the URL.</param> + /// <param name="scheme"> + /// The scheme to use for the URL. If null, the scheme will be selected automatically, + /// preferring HTTPS, if available. + /// </param> + /// <param name="port"> + /// The port to use for the URL. If null, the port will be selected automatically, + /// preferring the HTTPS port, if available. + /// </param> + /// <returns>The API URL.</returns> + string GetLocalApiUrl(ReadOnlySpan<char> hostname, string scheme = null, int? port = null); + /// <summary> /// Open a URL in an external browser window. /// </summary> @@ -97,7 +116,5 @@ namespace MediaBrowser.Controller string ReverseVirtualPath(string path); Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next); - - Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next); } } diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 5d7c60910a..c35a22ac70 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -71,7 +71,12 @@ namespace MediaBrowser.Controller string UserConfigurationDirectoryPath { get; } /// <summary> - /// Gets the internal metadata path. + /// Gets the default internal metadata path. + /// </summary> + string DefaultInternalMetadataPath { get; } + + /// <summary> + /// Gets the internal metadata path, either a custom path or the default. /// </summary> /// <value>The internal metadata path.</value> string InternalMetadataPath { get; } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 662ab25356..223bbe1ded 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Controller</PackageId> @@ -8,8 +13,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.4" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 8fefdd770f..61a3306756 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -459,7 +459,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions); var outputVideoCodec = GetVideoEncoder(state, encodingOptions); - + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; if (!hasTextSubs) @@ -2547,7 +2547,7 @@ namespace MediaBrowser.Controller.MediaEncoding encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); return null; } - return "-c:v h264_qsv "; + return "-c:v h264_qsv"; } break; case "hevc": @@ -2555,19 +2555,19 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.SupportsDecoder("hevc_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) { //return "-c:v hevc_qsv -load_plugin hevc_hw "; - return "-c:v hevc_qsv "; + return "-c:v hevc_qsv"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_qsv "; + return "-c:v mpeg2_qsv"; } break; case "vc1": if (_mediaEncoder.SupportsDecoder("vc1_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vc1_qsv "; + return "-c:v vc1_qsv"; } break; } @@ -2587,32 +2587,32 @@ namespace MediaBrowser.Controller.MediaEncoding encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); return null; } - return "-c:v h264_cuvid "; + return "-c:v h264_cuvid"; } break; case "hevc": case "h265": if (_mediaEncoder.SupportsDecoder("hevc_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) { - return "-c:v hevc_cuvid "; + return "-c:v hevc_cuvid"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_cuvid "; + return "-c:v mpeg2_cuvid"; } break; case "vc1": if (_mediaEncoder.SupportsDecoder("vc1_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vc1_cuvid "; + return "-c:v vc1_cuvid"; } break; case "mpeg4": if (_mediaEncoder.SupportsDecoder("mpeg4_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg4_cuvid "; + return "-c:v mpeg4_cuvid"; } break; } @@ -2626,38 +2626,38 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) { - return "-c:v h264_mediacodec "; + return "-c:v h264_mediacodec"; } break; case "hevc": case "h265": if (_mediaEncoder.SupportsDecoder("hevc_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase)) { - return "-c:v hevc_mediacodec "; + return "-c:v hevc_mediacodec"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_mediacodec "; + return "-c:v mpeg2_mediacodec"; } break; case "mpeg4": if (_mediaEncoder.SupportsDecoder("mpeg4_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg4_mediacodec "; + return "-c:v mpeg4_mediacodec"; } break; case "vp8": if (_mediaEncoder.SupportsDecoder("vp8_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vp8_mediacodec "; + return "-c:v vp8_mediacodec"; } break; case "vp9": if (_mediaEncoder.SupportsDecoder("vp9_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vp9_mediacodec "; + return "-c:v vp9_mediacodec"; } break; } @@ -2671,25 +2671,25 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) { - return "-c:v h264_mmal "; + return "-c:v h264_mmal"; } break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg2_mmal "; + return "-c:v mpeg2_mmal"; } break; case "mpeg4": if (_mediaEncoder.SupportsDecoder("mpeg4_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase)) { - return "-c:v mpeg4_mmal "; + return "-c:v mpeg4_mmal"; } break; case "vc1": if (_mediaEncoder.SupportsDecoder("vc1_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase)) { - return "-c:v vc1_mmal "; + return "-c:v vc1_mmal"; } break; } diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index b710318eee..1162bff130 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -77,8 +77,6 @@ namespace MediaBrowser.Controller.Net return Task.CompletedTask; } - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - /// <summary> /// Starts sending messages over a web socket /// </summary> @@ -87,12 +85,12 @@ namespace MediaBrowser.Controller.Net { var vals = message.Data.Split(','); - var dueTimeMs = long.Parse(vals[0], UsCulture); - var periodMs = long.Parse(vals[1], UsCulture); + var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture); + var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture); var cancellationTokenSource = new CancellationTokenSource(); - Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name); + Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name); var state = new TStateType { @@ -154,7 +152,6 @@ namespace MediaBrowser.Controller.Net { MessageType = Name, Data = data - }, cancellationToken).ConfigureAwait(false); state.DateLastSendUtc = DateTime.UtcNow; @@ -197,7 +194,7 @@ namespace MediaBrowser.Controller.Net /// <param name="connection">The connection.</param> private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection) { - Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name); + Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name); // TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really... // connection.Item1.Dispose(); @@ -242,6 +239,7 @@ namespace MediaBrowser.Controller.Net public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } } diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index 806478864d..efb5f4ac3f 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Events; using MediaBrowser.Model.Services; @@ -9,9 +8,9 @@ using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { /// <summary> - /// Interface IHttpServer + /// Interface IHttpServer. /// </summary> - public interface IHttpServer : IDisposable + public interface IHttpServer { /// <summary> /// Gets the URL prefix. @@ -19,11 +18,6 @@ namespace MediaBrowser.Controller.Net /// <value>The URL prefix.</value> string[] UrlPrefixes { get; } - /// <summary> - /// Stops this instance. - /// </summary> - void Stop(); - /// <summary> /// Occurs when [web socket connected]. /// </summary> @@ -40,22 +34,17 @@ namespace MediaBrowser.Controller.Net string GlobalResponse { get; set; } /// <summary> - /// Sends the http context to the socket listener + /// The HTTP request handler /// </summary> - /// <param name="ctx"></param> + /// <param name="context"></param> /// <returns></returns> - Task ProcessWebSocketRequest(HttpContext ctx); + Task RequestHandler(HttpContext context); /// <summary> - /// The HTTP request handler + /// Get the default CORS headers /// </summary> - /// <param name="httpReq"></param> - /// <param name="urlString"></param> - /// <param name="host"></param> - /// <param name="localPath"></param> - /// <param name="cancellationToken"></param> + /// <param name="req"></param> /// <returns></returns> - Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, - CancellationToken cancellationToken); + IDictionary<string, string> GetDefaultCorsHeaders(IRequest req); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index 31eb7ccb75..3ef8e5f6d4 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -1,4 +1,7 @@ +#nullable enable + using System; +using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; @@ -7,18 +10,12 @@ using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { - public interface IWebSocketConnection : IDisposable + public interface IWebSocketConnection { /// <summary> /// Occurs when [closed]. /// </summary> - event EventHandler<EventArgs> Closed; - - /// <summary> - /// Gets the id. - /// </summary> - /// <value>The id.</value> - Guid Id { get; } + event EventHandler<EventArgs>? Closed; /// <summary> /// Gets the last activity date. @@ -27,21 +24,22 @@ namespace MediaBrowser.Controller.Net DateTime LastActivityDate { get; } /// <summary> - /// Gets or sets the URL. + /// Gets or sets the date of last Keeplive received. /// </summary> - /// <value>The URL.</value> - string Url { get; set; } + /// <value>The date of last Keeplive received.</value> + DateTime LastKeepAliveDate { get; set; } + /// <summary> /// Gets or sets the query string. /// </summary> /// <value>The query string.</value> - IQueryCollection QueryString { get; set; } + IQueryCollection QueryString { get; } /// <summary> /// Gets or sets the receive action. /// </summary> /// <value>The receive action.</value> - Func<WebSocketMessageInfo, Task> OnReceive { get; set; } + Func<WebSocketMessageInfo, Task>? OnReceive { get; set; } /// <summary> /// Gets the state. @@ -53,7 +51,7 @@ namespace MediaBrowser.Controller.Net /// Gets the remote end point. /// </summary> /// <value>The remote end point.</value> - string RemoteEndPoint { get; } + IPAddress? RemoteEndPoint { get; } /// <summary> /// Sends a message asynchronously. @@ -65,21 +63,6 @@ namespace MediaBrowser.Controller.Net /// <exception cref="ArgumentNullException">message</exception> Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken); - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <param name="buffer">The buffer.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendAsync(byte[] buffer, CancellationToken cancellationToken); - - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <param name="text">The text.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">buffer</exception> - Task SendAsync(string text, CancellationToken cancellationToken); + Task ProcessAsync(CancellationToken cancellationToken = default); } } diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs index 3ccecf0eb8..a5b94ea5e3 100644 --- a/MediaBrowser.Controller/Net/SecurityException.cs +++ b/MediaBrowser.Controller/Net/SecurityException.cs @@ -2,20 +2,36 @@ using System; namespace MediaBrowser.Controller.Net { + /// <summary> + /// The exception that is thrown when a user is authenticated, but not authorized to access a requested resource. + /// </summary> public class SecurityException : Exception { + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + public SecurityException() + : base() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + /// <param name="message">The message that describes the error.</param> public SecurityException(string message) : base(message) { - } - public SecurityExceptionType SecurityExceptionType { get; set; } - } - - public enum SecurityExceptionType - { - Unauthenticated = 0, - ParentalControl = 1 + /// <summary> + /// Initializes a new instance of the <see cref="SecurityException"/> class. + /// </summary> + /// <param name="message">The message that describes the error</param> + /// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param> + public SecurityException(string message, Exception innerException) + : base(message, innerException) + { + } } } diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs index 4424e044bb..c2dcb66d7c 100644 --- a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs +++ b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs @@ -6,30 +6,34 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Persistence { /// <summary> - /// Interface IDisplayPreferencesRepository + /// Interface IDisplayPreferencesRepository. /// </summary> public interface IDisplayPreferencesRepository : IRepository { /// <summary> - /// Saves display preferences for an item + /// Saves display preferences for an item. /// </summary> /// <param name="displayPreferences">The display preferences.</param> /// <param name="userId">The user id.</param> /// <param name="client">The client.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, - CancellationToken cancellationToken); + void SaveDisplayPreferences( + DisplayPreferences displayPreferences, + string userId, + string client, + CancellationToken cancellationToken); /// <summary> - /// Saves all display preferences for a user + /// Saves all display preferences for a user. /// </summary> /// <param name="displayPreferences">The display preferences.</param> /// <param name="userId">The user id.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, - CancellationToken cancellationToken); + void SaveAllDisplayPreferences( + IEnumerable<DisplayPreferences> displayPreferences, + Guid userId, + CancellationToken cancellationToken); + /// <summary> /// Gets the display preferences. /// </summary> diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index a59c96ac70..04450085bf 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; @@ -20,6 +21,6 @@ namespace MediaBrowser.Controller.Session /// <summary> /// Sends the message. /// </summary> - Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken); + Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 74ffd5a181..efda51d4bf 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Events; using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; namespace MediaBrowser.Controller.Session { @@ -140,6 +141,24 @@ namespace MediaBrowser.Controller.Session /// <returns>Task.</returns> Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken); + /// <summary> + /// Sends the SyncPlayCommand. + /// </summary> + /// <param name="sessionId">The session id.</param> + /// <param name="command">The command.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken); + + /// <summary> + /// Sends the SyncPlayGroupUpdate. + /// </summary> + /// <param name="sessionId">The session id.</param> + /// <param name="command">The group update.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken); + /// <summary> /// Sends the browse command. /// </summary> diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index f1f10a3a3d..2ba7c9fec0 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Session { /// <summary> - /// Class SessionInfo + /// Class SessionInfo. /// </summary> - public class SessionInfo : IDisposable + public sealed class SessionInfo : IDisposable { - private ISessionManager _sessionManager; + // 1 second + private const long ProgressIncrement = 10000000; + + private readonly ISessionManager _sessionManager; private readonly ILogger _logger; + + private readonly object _progressLock = new object(); + private Timer _progressTimer; + private PlaybackProgressInfo _lastProgressInfo; + + private bool _disposed = false; + public SessionInfo(ISessionManager sessionManager, ILogger logger) { _sessionManager = sessionManager; @@ -97,8 +107,6 @@ namespace MediaBrowser.Controller.Session /// <value>The name of the device.</value> public string DeviceName { get; set; } - public string DeviceType { get; set; } - /// <summary> /// Gets or sets the now playing item. /// </summary> @@ -128,22 +136,6 @@ namespace MediaBrowser.Controller.Session [JsonIgnore] public ISessionController[] SessionControllers { get; set; } - /// <summary> - /// Gets or sets the supported commands. - /// </summary> - /// <value>The supported commands.</value> - public string[] SupportedCommands - { - get - { - if (Capabilities == null) - { - return new string[] { }; - } - return Capabilities.SupportedCommands; - } - } - public TranscodingInfo TranscodingInfo { get; set; } /// <summary> @@ -215,6 +207,14 @@ namespace MediaBrowser.Controller.Session } } + public QueueItem[] NowPlayingQueue { get; set; } + + public bool HasCustomDeviceName { get; set; } + + public string PlaylistItemId { get; set; } + + public string UserPrimaryImageTag { get; set; } + public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory) { var controllers = SessionControllers.ToList(); @@ -258,10 +258,6 @@ namespace MediaBrowser.Controller.Session return false; } - private readonly object _progressLock = new object(); - private Timer _progressTimer; - private PlaybackProgressInfo _lastProgressInfo; - public void StartAutomaticProgress(PlaybackProgressInfo progressInfo) { if (_disposed) @@ -284,9 +280,6 @@ namespace MediaBrowser.Controller.Session } } - // 1 second - private const long ProgressIncrement = 10000000; - private async void OnProgressTimerCallback(object state) { if (_disposed) @@ -345,8 +338,7 @@ namespace MediaBrowser.Controller.Session } } - private bool _disposed = false; - + /// <inheritdoc /> public void Dispose() { _disposed = true; @@ -358,30 +350,12 @@ namespace MediaBrowser.Controller.Session foreach (var controller in controllers) { - var disposable = controller as IDisposable; - - if (disposable != null) + if (controller is IDisposable disposable) { _logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name); - - try - { - disposable.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing session controller"); - } + disposable.Dispose(); } } - - _sessionManager = null; } - - public QueueItem[] NowPlayingQueue { get; set; } - public bool HasCustomDeviceName { get; set; } - public string PlaylistItemId { get; set; } - public string ServerId { get; set; } - public string UserPrimaryImageTag { get; set; } } } diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs new file mode 100644 index 0000000000..28a3ac505f --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Class GroupInfo. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public class GroupInfo + { + /// <summary> + /// Default ping value used for sessions. + /// </summary> + public long DefaulPing { get; } = 500; + + /// <summary> + /// Gets or sets the group identifier. + /// </summary> + /// <value>The group identifier.</value> + public Guid GroupId { get; } = Guid.NewGuid(); + + /// <summary> + /// Gets or sets the playing item. + /// </summary> + /// <value>The playing item.</value> + public BaseItem PlayingItem { get; set; } + + /// <summary> + /// Gets or sets whether playback is paused. + /// </summary> + /// <value>Playback is paused.</value> + public bool IsPaused { get; set; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long PositionTicks { get; set; } + + /// <summary> + /// Gets or sets the last activity. + /// </summary> + /// <value>The last activity.</value> + public DateTime LastActivity { get; set; } + + /// <summary> + /// Gets the participants. + /// </summary> + /// <value>The participants, or members of the group.</value> + public Dictionary<string, GroupMember> Participants { get; } = + new Dictionary<string, GroupMember>(StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// Checks if a session is in this group. + /// </summary> + /// <value><c>true</c> if the session is in this group; <c>false</c> otherwise.</value> + public bool ContainsSession(string sessionId) + { + return Participants.ContainsKey(sessionId); + } + + /// <summary> + /// Adds the session to the group. + /// </summary> + /// <param name="session">The session.</param> + public void AddSession(SessionInfo session) + { + if (ContainsSession(session.Id.ToString())) + { + return; + } + + var member = new GroupMember(); + member.Session = session; + member.Ping = DefaulPing; + member.IsBuffering = false; + Participants[session.Id.ToString()] = member; + } + + /// <summary> + /// Removes the session from the group. + /// </summary> + /// <param name="session">The session.</param> + public void RemoveSession(SessionInfo session) + { + if (!ContainsSession(session.Id.ToString())) + { + return; + } + + Participants.Remove(session.Id.ToString(), out _); + } + + /// <summary> + /// Updates the ping of a session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="ping">The ping.</param> + public void UpdatePing(SessionInfo session, long ping) + { + if (!ContainsSession(session.Id.ToString())) + { + return; + } + + Participants[session.Id.ToString()].Ping = ping; + } + + /// <summary> + /// Gets the highest ping in the group. + /// </summary> + /// <value name="session">The highest ping in the group.</value> + public long GetHighestPing() + { + long max = Int64.MinValue; + foreach (var session in Participants.Values) + { + max = Math.Max(max, session.Ping); + } + return max; + } + + /// <summary> + /// Sets the session's buffering state. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="isBuffering">The state.</param> + public void SetBuffering(SessionInfo session, bool isBuffering) + { + if (!ContainsSession(session.Id.ToString())) + { + return; + } + + Participants[session.Id.ToString()].IsBuffering = isBuffering; + } + + /// <summary> + /// Gets the group buffering state. + /// </summary> + /// <value><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</value> + public bool IsBuffering() + { + foreach (var session in Participants.Values) + { + if (session.IsBuffering) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Checks if the group is empty. + /// </summary> + /// <value><c>true</c> if the group is empty; <c>false</c> otherwise.</value> + public bool IsEmpty() + { + return Participants.Count == 0; + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs new file mode 100644 index 0000000000..a3975c334c --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Class GroupMember. + /// </summary> + public class GroupMember + { + /// <summary> + /// Gets or sets whether this member is buffering. + /// </summary> + /// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value> + public bool IsBuffering { get; set; } + + /// <summary> + /// Gets or sets the session. + /// </summary> + /// <value>The session.</value> + public SessionInfo Session { get; set; } + + /// <summary> + /// Gets or sets the ping. + /// </summary> + /// <value>The ping.</value> + public long Ping { get; set; } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs new file mode 100644 index 0000000000..de1fcd2591 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Interface ISyncPlayController. + /// </summary> + public interface ISyncPlayController + { + /// <summary> + /// Gets the group id. + /// </summary> + /// <value>The group id.</value> + Guid GetGroupId(); + + /// <summary> + /// Gets the playing item id. + /// </summary> + /// <value>The playing item id.</value> + Guid GetPlayingItemId(); + + /// <summary> + /// Checks if the group is empty. + /// </summary> + /// <value>If the group is empty.</value> + bool IsGroupEmpty(); + + /// <summary> + /// Initializes the group with the session's info. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void InitGroup(SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Adds the session to the group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Removes the session from the group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SessionLeave(SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles the requested action by the session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The requested action.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Gets the info about the group for the clients. + /// </summary> + /// <value>The group info for the clients.</value> + GroupInfoView GetInfo(); + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs new file mode 100644 index 0000000000..006fb687b8 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// <summary> + /// Interface ISyncPlayManager. + /// </summary> + public interface ISyncPlayManager + { + /// <summary> + /// Creates a new group. + /// </summary> + /// <param name="session">The session that's creating the group.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void NewGroup(SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Adds the session to a group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="groupId">The group id.</param> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void JoinGroup(SessionInfo session, Guid groupId, JoinGroupRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Removes the session from a group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void LeaveGroup(SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Gets list of available groups for a session. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="filterItemId">The item id to filter by.</param> + /// <value>The list of available groups.</value> + List<GroupInfoView> ListGroups(SessionInfo session, Guid filterItemId); + + /// <summary> + /// Handle a request by a session in a group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + + /// <summary> + /// Maps a session to a group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="group">The group.</param> + /// <exception cref="InvalidOperationException"></exception> + void AddSessionToGroup(SessionInfo session, ISyncPlayController group); + + /// <summary> + /// Unmaps a session from a group. + /// </summary> + /// <param name="session">The session.</param> + /// <param name="group">The group.</param> + /// <exception cref="InvalidOperationException"></exception> + void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group); + } +} diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 71eb62693c..24104d779d 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}</ProjectGuid> + </PropertyGroup> + <ItemGroup> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index c2bfac9d63..1377502dd9 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -19,7 +19,6 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.System; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Diagnostics; @@ -39,8 +38,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; - private readonly Func<ISubtitleEncoder> _subtitleEncoder; - private readonly IConfiguration _configuration; + private readonly Lazy<EncodingHelper> _encodingHelperFactory; private readonly string _startupOptionFFmpegPath; private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); @@ -48,8 +46,6 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly object _runningProcessesLock = new object(); private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>(); - private EncodingHelper _encodingHelper; - private string _ffmpegPath; private string _ffprobePath; @@ -58,23 +54,18 @@ namespace MediaBrowser.MediaEncoding.Encoder IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, - Func<ISubtitleEncoder> subtitleEncoder, - IConfiguration configuration, + Lazy<EncodingHelper> encodingHelperFactory, string startupOptionsFFmpegPath) { _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; _localization = localization; + _encodingHelperFactory = encodingHelperFactory; _startupOptionFFmpegPath = startupOptionsFFmpegPath; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; } - private EncodingHelper EncodingHelper - => LazyInitializer.EnsureInitialized( - ref _encodingHelper, - () => new EncodingHelper(this, _fileSystem, _subtitleEncoder(), _configuration)); + private EncodingHelper EncodingHelper => _encodingHelperFactory.Value; /// <inheritdoc /> public string EncoderPath => _ffmpegPath; @@ -122,7 +113,7 @@ namespace MediaBrowser.MediaEncoding.Encoder SetAvailableEncoders(validator.GetEncoders()); } - _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, _ffmpegPath ?? string.Empty); + _logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty); } /// <summary> @@ -135,7 +126,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { string newPath; - _logger.LogInformation("Attempting to update encoder path to {0}. pathType: {1}", path ?? string.Empty, pathType ?? string.Empty); + _logger.LogInformation("Attempting to update encoder path to {Path}. pathType: {PathType}", path ?? string.Empty, pathType ?? string.Empty); if (!string.Equals(pathType, "custom", StringComparison.OrdinalIgnoreCase)) { @@ -189,7 +180,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!rc) { - _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location, path); + _logger.LogWarning("FFmpeg: {Location}: Failed version check: {Path}", location, path); } // ToDo - Enable the ffmpeg validator. At the moment any version can be used. @@ -200,18 +191,18 @@ namespace MediaBrowser.MediaEncoding.Encoder } else { - _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location, path); + _logger.LogWarning("FFmpeg: {Location}: File not found: {Path}", location, path); } } return rc; } - private string GetEncoderPathFromDirectory(string path, string filename) + private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false) { try { - var files = _fileSystem.GetFilePaths(path); + var files = _fileSystem.GetFilePaths(path, recursive); var excludeExtensions = new[] { ".c" }; @@ -232,7 +223,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <returns></returns> private string ExistsOnSystemPath(string fileName) { - string inJellyfinPath = GetEncoderPathFromDirectory(System.AppContext.BaseDirectory, fileName); + var inJellyfinPath = GetEncoderPathFromDirectory(AppContext.BaseDirectory, fileName, recursive: true); if (!string.IsNullOrEmpty(inJellyfinPath)) { return inJellyfinPath; @@ -478,7 +469,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } catch (Exception ex) { - _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {arguments}", inputArgument); + _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument); } } @@ -901,7 +892,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return minSizeVobs.Count == 0 ? vobs.Select(i => i.FullName) : minSizeVobs.Select(i => i.FullName); } - _logger.LogWarning("Could not determine vob file list for {0} using DvdLib. Will scan using file sizes.", path); + _logger.LogWarning("Could not determine vob file list for {Path} using DvdLib. Will scan using file sizes.", path); } var files = allVobs @@ -969,7 +960,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public int? ExitCode { get; private set; } - void OnProcessExited(object sender, EventArgs e) + private void OnProcessExited(object sender, EventArgs e) { var process = (Process)sender; diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index a312dcd705..af8bee301c 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{960295EE-4AF4-4440-A525-B4C295B01A61}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index 0b2f1d231e..a2ea0766a8 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -278,5 +278,19 @@ namespace MediaBrowser.MediaEncoding.Probing /// <value>The disposition.</value> [JsonPropertyName("disposition")] public IReadOnlyDictionary<string, int> Disposition { get; set; } + + /// <summary> + /// Gets or sets the color transfer. + /// </summary> + /// <value>The color transfer.</value> + [JsonPropertyName("color_transfer")] + public string ColorTransfer { get; set; } + + /// <summary> + /// Gets or sets the color primaries. + /// </summary> + /// <value>The color primaries.</value> + [JsonPropertyName("color_primaries")] + public string ColorPrimaries { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index b24d97f4ef..d3f8094b94 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -695,6 +695,16 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.RefFrames = streamInfo.Refs; } + + if (!string.IsNullOrEmpty(streamInfo.ColorTransfer)) + { + stream.ColorTransfer = streamInfo.ColorTransfer; + } + + if (!string.IsNullOrEmpty(streamInfo.ColorPrimaries)) + { + stream.ColorPrimaries = streamInfo.ColorPrimaries; + } } else { diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs index 2e328ba63e..de35acbba9 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs @@ -7,14 +7,25 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles { + /// <summary> + /// Subtitle writer for the WebVTT format. + /// </summary> public class VttWriter : ISubtitleWriter { + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { writer.WriteLine("WEBVTT"); writer.WriteLine(string.Empty); + writer.WriteLine("REGION"); + writer.WriteLine("id:subtitle"); + writer.WriteLine("width:80%"); + writer.WriteLine("lines:3"); + writer.WriteLine("regionanchor:50%,100%"); + writer.WriteLine("viewportanchor:50%,90%"); + writer.WriteLine(string.Empty); foreach (var trackEvent in info.TrackEvents) { cancellationToken.ThrowIfCancellationRequested(); @@ -22,13 +33,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks); var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks); - // make sure the start and end times are different and seqential + // make sure the start and end times are different and sequential if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds) { endTime = startTime.Add(TimeSpan.FromMilliseconds(1)); } - writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff}", startTime, endTime); + writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle", startTime, endTime); var text = trackEvent.Text; diff --git a/MediaBrowser.Model/Activity/ActivityLogEntry.cs b/MediaBrowser.Model/Activity/ActivityLogEntry.cs index 80f01b66ee..5ab904394e 100644 --- a/MediaBrowser.Model/Activity/ActivityLogEntry.cs +++ b/MediaBrowser.Model/Activity/ActivityLogEntry.cs @@ -59,6 +59,7 @@ namespace MediaBrowser.Model.Activity /// Gets or sets the user primary image tag. /// </summary> /// <value>The user primary image tag.</value> + [Obsolete("UserPrimaryImageTag is not used.")] public string UserPrimaryImageTag { get; set; } /// <summary> diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index f336f5272c..9dab5e77b7 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,6 +1,9 @@ #pragma warning disable CS1591 using System; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; @@ -10,10 +13,15 @@ namespace MediaBrowser.Model.Activity { event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; - void Create(ActivityLogEntry entry); + void Create(ActivityLog entry); - QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit); + Task CreateAsync(ActivityLog entry); - QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? x, int? y); + QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit); + + QueryResult<ActivityLogEntry> GetPagedResult( + Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func, + int? startIndex, + int? limit); } } diff --git a/MediaBrowser.Model/Activity/IActivityRepository.cs b/MediaBrowser.Model/Activity/IActivityRepository.cs deleted file mode 100644 index 66144ec478..0000000000 --- a/MediaBrowser.Model/Activity/IActivityRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Querying; - -namespace MediaBrowser.Model.Activity -{ - public interface IActivityRepository - { - void Create(ActivityLogEntry entry); - - QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? z, int? startIndex, int? limit); - } -} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 3107ec2426..1f5981f101 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -15,11 +15,15 @@ namespace MediaBrowser.Model.Configuration private string _baseUrl; /// <summary> - /// Gets or sets a value indicating whether [enable u pn p]. + /// Gets or sets a value indicating whether to enable automatic port forwarding. /// </summary> - /// <value><c>true</c> if [enable u pn p]; otherwise, <c>false</c>.</value> public bool EnableUPnP { get; set; } + /// <summary> + /// Gets or sets a value indicating whether to enable prometheus metrics exporting. + /// </summary> + public bool EnableMetrics { get; set; } + /// <summary> /// Gets or sets the public mapped port. /// </summary> @@ -45,17 +49,24 @@ namespace MediaBrowser.Model.Configuration public int HttpsPortNumber { get; set; } /// <summary> - /// Gets or sets a value indicating whether [use HTTPS]. + /// Gets or sets a value indicating whether to use HTTPS. /// </summary> - /// <value><c>true</c> if [use HTTPS]; otherwise, <c>false</c>.</value> + /// <remarks> + /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be + /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>. + /// </remarks> public bool EnableHttps { get; set; } + public bool EnableNormalizedItemByNameIds { get; set; } /// <summary> - /// Gets or sets the value pointing to the file system where the ssl certificate is located.. + /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. /// </summary> - /// <value>The value pointing to the file system where the ssl certificate is located..</value> public string CertificatePath { get; set; } + + /// <summary> + /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>. + /// </summary> public string CertificatePassword { get; set; } /// <summary> @@ -65,8 +76,9 @@ namespace MediaBrowser.Model.Configuration public bool IsPortAuthorized { get; set; } public bool AutoRunWebApp { get; set; } + public bool EnableRemoteAccess { get; set; } - public bool CameraUploadUpgraded { get; set; } + public bool CollectionsUpgraded { get; set; } /// <summary> @@ -82,6 +94,7 @@ namespace MediaBrowser.Model.Configuration /// </summary> /// <value>The metadata path.</value> public string MetadataPath { get; set; } + public string MetadataNetworkPath { get; set; } /// <summary> @@ -204,15 +217,26 @@ namespace MediaBrowser.Model.Configuration public int RemoteClientBitrateLimit { get; set; } public bool EnableFolderView { get; set; } + public bool EnableGroupingIntoCollections { get; set; } + public bool DisplaySpecialsWithinSeasons { get; set; } + public string[] LocalNetworkSubnets { get; set; } + public string[] LocalNetworkAddresses { get; set; } + public string[] CodecsUsed { get; set; } + public bool IgnoreVirtualInterfaces { get; set; } + public bool EnableExternalContentInSuggestions { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the server should force connections over HTTPS. + /// </summary> public bool RequireHttps { get; set; } - public bool IsBehindProxy { get; set; } + public bool EnableNewOmdbSupport { get; set; } public string[] RemoteIPFilter { get; set; } @@ -247,6 +271,7 @@ namespace MediaBrowser.Model.Configuration PublicHttpsPort = DefaultHttpsPort; HttpServerPortNumber = DefaultHttpPort; HttpsPortNumber = DefaultHttpsPort; + EnableMetrics = false; EnableHttps = false; EnableDashboardResponseCaching = true; EnableCaseSensitiveItemIds = true; @@ -254,7 +279,7 @@ namespace MediaBrowser.Model.Configuration AutoRunWebApp = true; EnableRemoteAccess = true; - EnableUPnP = true; + EnableUPnP = false; MinResumePct = 5; MaxResumePct = 90; diff --git a/MediaBrowser.Model/Configuration/SyncplayAccess.cs b/MediaBrowser.Model/Configuration/SyncplayAccess.cs new file mode 100644 index 0000000000..d891a8167a --- /dev/null +++ b/MediaBrowser.Model/Configuration/SyncplayAccess.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Model.Configuration +{ + /// <summary> + /// Enum SyncPlayAccess. + /// </summary> + public enum SyncPlayAccess + { + /// <summary> + /// User can create groups and join them. + /// </summary> + CreateAndJoinGroups, + + /// <summary> + /// User can only join already existing groups. + /// </summary> + JoinGroups, + + /// <summary> + /// SyncPlay is disabled for the user. + /// </summary> + None + } +} diff --git a/MediaBrowser.Model/Devices/DeviceOptions.cs b/MediaBrowser.Model/Devices/DeviceOptions.cs new file mode 100644 index 0000000000..8b77fd7fc3 --- /dev/null +++ b/MediaBrowser.Model/Devices/DeviceOptions.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Model.Devices +{ + public class DeviceOptions + { + public string CustomName { get; set; } + } +} diff --git a/MediaBrowser.Model/Devices/DevicesOptions.cs b/MediaBrowser.Model/Devices/DevicesOptions.cs deleted file mode 100644 index 02570650e9..0000000000 --- a/MediaBrowser.Model/Devices/DevicesOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Devices -{ - public class DevicesOptions - { - public string[] EnabledCameraUploadDevices { get; set; } - public string CameraUploadPath { get; set; } - public bool EnableCameraUploadSubfolders { get; set; } - - public DevicesOptions() - { - EnabledCameraUploadDevices = Array.Empty<string>(); - } - } - - public class DeviceOptions - { - public string CustomName { get; set; } - } -} diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs index 756e500dd7..7bb961deb2 100644 --- a/MediaBrowser.Model/Dlna/CodecProfile.cs +++ b/MediaBrowser.Model/Dlna/CodecProfile.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 using System; +using System.Linq; using System.Xml.Serialization; -using MediaBrowser.Model.Extensions; namespace MediaBrowser.Model.Dlna { @@ -57,7 +57,7 @@ namespace MediaBrowser.Model.Dlna foreach (var val in codec) { - if (ListHelper.ContainsIgnoreCase(codecs, val)) + if (codecs.Contains(val, StringComparer.OrdinalIgnoreCase)) { return true; } diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index 7423efaf65..0c3bd88829 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 using System; +using System.Linq; using System.Globalization; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna @@ -167,9 +167,7 @@ namespace MediaBrowser.Model.Dlna switch (condition.Condition) { case ProfileConditionType.EqualsAny: - { - return ListHelper.ContainsIgnoreCase(expected.Split('|'), currentValue); - } + return expected.Split('|').Contains(currentValue, StringComparer.OrdinalIgnoreCase); case ProfileConditionType.Equals: return string.Equals(currentValue, expected, StringComparison.OrdinalIgnoreCase); case ProfileConditionType.NotEquals: diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index e6691c5139..cc2417a709 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 using System; +using System.Linq; using System.Xml.Serialization; -using MediaBrowser.Model.Extensions; namespace MediaBrowser.Model.Dlna { @@ -45,7 +45,7 @@ namespace MediaBrowser.Model.Dlna public static bool ContainsContainer(string profileContainers, string inputContainer) { var isNegativeList = false; - if (profileContainers != null && profileContainers.StartsWith("-")) + if (profileContainers != null && profileContainers.StartsWith("-", StringComparison.Ordinal)) { isNegativeList = true; profileContainers = profileContainers.Substring(1); @@ -72,7 +72,7 @@ namespace MediaBrowser.Model.Dlna foreach (var container in allInputContainers) { - if (ListHelper.ContainsIgnoreCase(profileContainers, container)) + if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase)) { return false; } @@ -86,7 +86,7 @@ namespace MediaBrowser.Model.Dlna foreach (var container in allInputContainers) { - if (ListHelper.ContainsIgnoreCase(profileContainers, container)) + if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase)) { return true; } diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 0cefbbe012..3813ac5ebb 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 using System; +using System.Linq; using System.Xml.Serialization; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna @@ -93,14 +93,14 @@ namespace MediaBrowser.Model.Dlna public DeviceProfile() { - DirectPlayProfiles = new DirectPlayProfile[] { }; - TranscodingProfiles = new TranscodingProfile[] { }; - ResponseProfiles = new ResponseProfile[] { }; - CodecProfiles = new CodecProfile[] { }; - ContainerProfiles = new ContainerProfile[] { }; + DirectPlayProfiles = Array.Empty<DirectPlayProfile>(); + TranscodingProfiles = Array.Empty<TranscodingProfile>(); + ResponseProfiles = Array.Empty<ResponseProfile>(); + CodecProfiles = Array.Empty<CodecProfile>(); + ContainerProfiles = Array.Empty<ContainerProfile>(); SubtitleProfiles = Array.Empty<SubtitleProfile>(); - XmlRootAttributes = new XmlAttribute[] { }; + XmlRootAttributes = Array.Empty<XmlAttribute>(); SupportedMediaTypes = "Audio,Photo,Video"; MaxStreamingBitrate = 8000000; @@ -129,13 +129,14 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty)) + if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { continue; } return i; } + return null; } @@ -155,7 +156,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty)) + if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { continue; } @@ -185,7 +186,7 @@ namespace MediaBrowser.Model.Dlna } var audioCodecs = i.GetAudioCodecs(); - if (audioCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty)) + if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { continue; } @@ -288,13 +289,13 @@ namespace MediaBrowser.Model.Dlna } var audioCodecs = i.GetAudioCodecs(); - if (audioCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty)) + if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { continue; } var videoCodecs = i.GetVideoCodecs(); - if (videoCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(videoCodecs, videoCodec ?? string.Empty)) + if (videoCodecs.Length > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs index 6a8f655ac5..9c28019aad 100644 --- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs +++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs @@ -1,7 +1,8 @@ #pragma warning disable CS1591 +using System; +using System.Linq; using System.Xml.Serialization; -using MediaBrowser.Model.Extensions; namespace MediaBrowser.Model.Dlna { @@ -40,7 +41,7 @@ namespace MediaBrowser.Model.Dlna } var languages = GetLanguages(); - return languages.Length == 0 || ListHelper.ContainsIgnoreCase(languages, subLanguage); + return languages.Length == 0 || languages.Contains(subLanguage, StringComparer.OrdinalIgnoreCase); } } } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index e7e8d7cecd..ac33f1da42 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -34,8 +34,22 @@ namespace MediaBrowser.Model.Entities /// <value>The language.</value> public string Language { get; set; } + /// <summary> + /// Gets or sets the color transfer. + /// </summary> + /// <value>The color transfer.</value> public string ColorTransfer { get; set; } + + /// <summary> + /// Gets or sets the color primaries. + /// </summary> + /// <value>The color primaries.</value> public string ColorPrimaries { get; set; } + + /// <summary> + /// Gets or sets the color space. + /// </summary> + /// <value>The color space.</value> public string ColorSpace { get; set; } /// <summary> @@ -44,11 +58,28 @@ namespace MediaBrowser.Model.Entities /// <value>The comment.</value> public string Comment { get; set; } + /// <summary> + /// Gets or sets the time base. + /// </summary> + /// <value>The time base.</value> public string TimeBase { get; set; } + + /// <summary> + /// Gets or sets the codec time base. + /// </summary> + /// <value>The codec time base.</value> public string CodecTimeBase { get; set; } + /// <summary> + /// Gets or sets the title. + /// </summary> + /// <value>The title.</value> public string Title { get; set; } + /// <summary> + /// Gets or sets the video range. + /// </summary> + /// <value>The video range.</value> public string VideoRange { get @@ -60,7 +91,8 @@ namespace MediaBrowser.Model.Entities var colorTransfer = ColorTransfer; - if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + || string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) { return "HDR"; } @@ -70,7 +102,9 @@ namespace MediaBrowser.Model.Entities } public string localizedUndefined { get; set; } + public string localizedDefault { get; set; } + public string localizedForced { get; set; } public string DisplayTitle @@ -197,34 +231,34 @@ namespace MediaBrowser.Model.Entities { if (i.IsInterlaced) { - return "1440I"; + return "1440i"; } - return "1440P"; + return "1440p"; } if (width >= 1900 || height >= 1000) { if (i.IsInterlaced) { - return "1080I"; + return "1080i"; } - return "1080P"; + return "1080p"; } if (width >= 1260 || height >= 700) { if (i.IsInterlaced) { - return "720I"; + return "720i"; } - return "720P"; + return "720p"; } if (width >= 700 || height >= 440) { if (i.IsInterlaced) { - return "480I"; + return "480i"; } - return "480P"; + return "480p"; } return "SD"; diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs index cd387bd546..922eb4ca79 100644 --- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs +++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Model.Entities } /// <summary> - /// Gets a provider id + /// Gets a provider id. /// </summary> /// <param name="instance">The instance.</param> /// <param name="provider">The provider.</param> @@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Entities } /// <summary> - /// Gets a provider id + /// Gets a provider id. /// </summary> /// <param name="instance">The instance.</param> /// <param name="name">The name.</param> @@ -53,7 +53,7 @@ namespace MediaBrowser.Model.Entities } /// <summary> - /// Sets a provider id + /// Sets a provider id. /// </summary> /// <param name="instance">The instance.</param> /// <param name="name">The name.</param> @@ -89,7 +89,7 @@ namespace MediaBrowser.Model.Entities } /// <summary> - /// Sets a provider id + /// Sets a provider id. /// </summary> /// <param name="instance">The instance.</param> /// <param name="provider">The provider.</param> diff --git a/MediaBrowser.Model/Extensions/ListHelper.cs b/MediaBrowser.Model/Extensions/ListHelper.cs deleted file mode 100644 index 90ce6f2e5e..0000000000 --- a/MediaBrowser.Model/Extensions/ListHelper.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Extensions -{ - // TODO: @bond remove - public static class ListHelper - { - public static bool ContainsIgnoreCase(string[] list, string value) - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - foreach (var item in list) - { - if (string.Equals(item, value, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; - } - } -} diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs index e30dd84dcd..29f417489a 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Model.LiveTv public SeriesTimerInfoDto() { ImageTags = new Dictionary<ImageType, string>(); - Days = new DayOfWeek[] { }; + Days = Array.Empty<DayOfWeek>(); Type = "SeriesTimer"; } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 27486c68f1..461f59672e 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Model</PackageId> @@ -16,9 +21,9 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.4" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> - <PackageReference Include="System.Text.Json" Version="4.7.1" /> + <PackageReference Include="System.Text.Json" Version="4.7.2" /> </ItemGroup> <ItemGroup> @@ -32,6 +37,9 @@ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" /> + </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 68bcc590c4..b6d7b4245c 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -17,112 +17,134 @@ namespace MediaBrowser.Model.Net /// </summary> private static readonly HashSet<string> _videoFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { - ".mkv", - ".m2t", - ".m2ts", + ".3gp", + ".asf", + ".avi", + ".divx", + ".dvr-ms", + ".f4v", + ".flv", ".img", ".iso", + ".m2t", + ".m2ts", + ".m2v", + ".m4v", ".mk3d", - ".ts", - ".rmvb", + ".mkv", ".mov", - ".avi", + ".mp4", ".mpg", ".mpeg", - ".wmv", - ".mp4", - ".divx", - ".dvr-ms", - ".wtv", + ".mts", + ".ogg", ".ogm", ".ogv", - ".asf", - ".m4v", - ".flv", - ".f4v", - ".3gp", + ".rec", + ".ts", + ".rmvb", ".webm", - ".mts", - ".m2v", - ".rec" + ".wmv", + ".wtv", }; // http://en.wikipedia.org/wiki/Internet_media_type + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + // http://www.iana.org/assignments/media-types/media-types.xhtml // Add more as needed private static readonly Dictionary<string, string> _mimeTypeLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { // Type application + { ".7z", "application/x-7z-compressed" }, + { ".azw", "application/vnd.amazon.ebook" }, + { ".azw3", "application/vnd.amazon.ebook" }, { ".cbz", "application/x-cbz" }, { ".cbr", "application/epub+zip" }, { ".eot", "application/vnd.ms-fontobject" }, { ".epub", "application/epub+zip" }, { ".js", "application/x-javascript" }, { ".json", "application/json" }, + { ".m3u8", "application/x-mpegURL" }, { ".map", "application/x-javascript" }, + { ".mobi", "application/x-mobipocket-ebook" }, + { ".opf", "application/oebps-package+xml" }, { ".pdf", "application/pdf" }, + { ".rar", "application/vnd.rar" }, + { ".srt", "application/x-subrip" }, { ".ttml", "application/ttml+xml" }, - { ".m3u8", "application/x-mpegURL" }, - { ".mobi", "application/x-mobipocket-ebook" }, - { ".xml", "application/xml" }, { ".wasm", "application/wasm" }, + { ".xml", "application/xml" }, + { ".zip", "application/zip" }, // Type image + { ".bmp", "image/bmp" }, + { ".gif", "image/gif" }, + { ".ico", "image/vnd.microsoft.icon" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" }, - { ".tbn", "image/jpeg" }, { ".png", "image/png" }, - { ".gif", "image/gif" }, - { ".tiff", "image/tiff" }, - { ".webp", "image/webp" }, - { ".ico", "image/vnd.microsoft.icon" }, { ".svg", "image/svg+xml" }, { ".svgz", "image/svg+xml" }, + { ".tbn", "image/jpeg" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".webp", "image/webp" }, // Type font { ".ttf" , "font/ttf" }, { ".woff" , "font/woff" }, + { ".woff2" , "font/woff2" }, // Type text { ".ass", "text/x-ssa" }, { ".ssa", "text/x-ssa" }, { ".css", "text/css" }, { ".csv", "text/csv" }, + { ".edl", "text/plain" }, + { ".rtf", "text/rtf" }, { ".txt", "text/plain" }, { ".vtt", "text/vtt" }, // Type video - { ".mpg", "video/mpeg" }, - { ".ogv", "video/ogg" }, - { ".mov", "video/quicktime" }, - { ".webm", "video/webm" }, - { ".mkv", "video/x-matroska" }, - { ".wmv", "video/x-ms-wmv" }, - { ".flv", "video/x-flv" }, - { ".avi", "video/x-msvideo" }, - { ".asf", "video/x-ms-asf" }, - { ".m4v", "video/x-m4v" }, - { ".m4s", "video/mp4" }, { ".3gp", "video/3gpp" }, { ".3g2", "video/3gpp2" }, + { ".asf", "video/x-ms-asf" }, + { ".avi", "video/x-msvideo" }, + { ".flv", "video/x-flv" }, + { ".mp4", "video/mp4" }, + { ".m4s", "video/mp4" }, + { ".m4v", "video/x-m4v" }, + { ".mpegts", "video/mp2t" }, + { ".mpg", "video/mpeg" }, + { ".mkv", "video/x-matroska" }, + { ".mov", "video/quicktime" }, { ".mpd", "video/vnd.mpeg.dash.mpd" }, + { ".ogv", "video/ogg" }, { ".ts", "video/mp2t" }, + { ".webm", "video/webm" }, + { ".wmv", "video/x-ms-wmv" }, // Type audio - { ".mp3", "audio/mpeg" }, - { ".m4a", "audio/mp4" }, { ".aac", "audio/mp4" }, - { ".webma", "audio/webm" }, - { ".wav", "audio/wav" }, - { ".wma", "audio/x-ms-wma" }, - { ".ogg", "audio/ogg" }, - { ".oga", "audio/ogg" }, - { ".opus", "audio/ogg" }, { ".ac3", "audio/ac3" }, + { ".ape", "audio/x-ape" }, { ".dsf", "audio/dsf" }, - { ".m4b", "audio/m4b" }, - { ".xsp", "audio/xsp" }, { ".dsp", "audio/dsp" }, { ".flac", "audio/flac" }, + { ".m4a", "audio/mp4" }, + { ".m4b", "audio/m4b" }, + { ".mid", "audio/midi" }, + { ".midi", "audio/midi" }, + { ".mp3", "audio/mpeg" }, + { ".oga", "audio/ogg" }, + { ".ogg", "audio/ogg" }, + { ".opus", "audio/ogg" }, + { ".vorbis", "audio/vorbis" }, + { ".wav", "audio/wav" }, + { ".webma", "audio/webm" }, + { ".wma", "audio/x-ms-wma" }, + { ".wv", "audio/x-wavpack" }, + { ".xsp", "audio/xsp" }, }; private static readonly Dictionary<string, string> _extensionLookup = CreateExtensionLookup(); diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs index 7575224d4e..03f03e4ccd 100644 --- a/MediaBrowser.Model/Net/WebSocketMessage.cs +++ b/MediaBrowser.Model/Net/WebSocketMessage.cs @@ -1,5 +1,8 @@ + #pragma warning disable CS1591 +using System; + namespace MediaBrowser.Model.Net { /// <summary> @@ -13,7 +16,9 @@ namespace MediaBrowser.Model.Net /// </summary> /// <value>The type of the message.</value> public string MessageType { get; set; } - public string MessageId { get; set; } + + public Guid MessageId { get; set; } + public string ServerId { get; set; } /// <summary> @@ -22,5 +27,4 @@ namespace MediaBrowser.Model.Net /// <value>The data.</value> public T Data { get; set; } } - } diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs index 79a128e9be..9c54bd70e0 100644 --- a/MediaBrowser.Model/Notifications/NotificationOptions.cs +++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Extensions; +using System.Linq; using MediaBrowser.Model.Users; namespace MediaBrowser.Model.Notifications @@ -81,8 +81,12 @@ namespace MediaBrowser.Model.Notifications { foreach (NotificationOption i in Options) { - if (string.Equals(type, i.Type, StringComparison.OrdinalIgnoreCase)) return i; + if (string.Equals(type, i.Type, StringComparison.OrdinalIgnoreCase)) + { + return i; + } } + return null; } @@ -98,7 +102,7 @@ namespace MediaBrowser.Model.Notifications NotificationOption opt = GetOptions(notificationType); return opt == null || - !ListHelper.ContainsIgnoreCase(opt.DisabledServices, service); + !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase); } public bool IsEnabledToMonitorUser(string type, Guid userId) @@ -106,7 +110,7 @@ namespace MediaBrowser.Model.Notifications NotificationOption opt = GetOptions(type); return opt != null && opt.Enabled && - !ListHelper.ContainsIgnoreCase(opt.DisabledMonitorUsers, userId.ToString("")); + !opt.DisabledMonitorUsers.Contains(userId.ToString(""), StringComparer.OrdinalIgnoreCase); } public bool IsEnabledToSendToUser(string type, string userId, UserPolicy userPolicy) @@ -125,7 +129,7 @@ namespace MediaBrowser.Model.Notifications return true; } - return ListHelper.ContainsIgnoreCase(opt.SendToUsers, userId); + return opt.SendToUsers.Contains(userId, StringComparer.OrdinalIgnoreCase); } return false; diff --git a/MediaBrowser.Model/Services/IHasRequestFilter.cs b/MediaBrowser.Model/Services/IHasRequestFilter.cs index 3d2e9c0dcf..3e49e9872d 100644 --- a/MediaBrowser.Model/Services/IHasRequestFilter.cs +++ b/MediaBrowser.Model/Services/IHasRequestFilter.cs @@ -8,17 +8,17 @@ namespace MediaBrowser.Model.Services { /// <summary> /// Order in which Request Filters are executed. - /// <0 Executed before global request filters - /// >0 Executed after global request filters + /// <0 Executed before global request filters. + /// >0 Executed after global request filters. /// </summary> int Priority { get; } /// <summary> /// The request filter is executed before the service. /// </summary> - /// <param name="req">The http request wrapper</param> - /// <param name="res">The http response wrapper</param> - /// <param name="requestDto">The request DTO</param> + /// <param name="req">The http request wrapper.</param> + /// <param name="res">The http response wrapper.</param> + /// <param name="requestDto">The request DTO.</param> void RequestFilter(IRequest req, HttpResponse res, object requestDto); } } diff --git a/MediaBrowser.Model/SyncPlay/GroupInfoView.cs b/MediaBrowser.Model/SyncPlay/GroupInfoView.cs new file mode 100644 index 0000000000..f28ecf16df --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupInfoView.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Class GroupInfoView. + /// </summary> + public class GroupInfoView + { + /// <summary> + /// Gets or sets the group identifier. + /// </summary> + /// <value>The group identifier.</value> + public string GroupId { get; set; } + + /// <summary> + /// Gets or sets the playing item id. + /// </summary> + /// <value>The playing item id.</value> + public string PlayingItemId { get; set; } + + /// <summary> + /// Gets or sets the playing item name. + /// </summary> + /// <value>The playing item name.</value> + public string PlayingItemName { get; set; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long PositionTicks { get; set; } + + /// <summary> + /// Gets or sets the participants. + /// </summary> + /// <value>The participants.</value> + public IReadOnlyList<string> Participants { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdate.cs b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs new file mode 100644 index 0000000000..895702f3dd --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs @@ -0,0 +1,26 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Class GroupUpdate. + /// </summary> + public class GroupUpdate<T> + { + /// <summary> + /// Gets or sets the group identifier. + /// </summary> + /// <value>The group identifier.</value> + public string GroupId { get; set; } + + /// <summary> + /// Gets or sets the update type. + /// </summary> + /// <value>The update type.</value> + public GroupUpdateType Type { get; set; } + + /// <summary> + /// Gets or sets the data. + /// </summary> + /// <value>The data.</value> + public T Data { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs new file mode 100644 index 0000000000..89d2457872 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs @@ -0,0 +1,53 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Enum GroupUpdateType. + /// </summary> + public enum GroupUpdateType + { + /// <summary> + /// The user-joined update. Tells members of a group about a new user. + /// </summary> + UserJoined, + /// <summary> + /// The user-left update. Tells members of a group that a user left. + /// </summary> + UserLeft, + /// <summary> + /// The group-joined update. Tells a user that the group has been joined. + /// </summary> + GroupJoined, + /// <summary> + /// The group-left update. Tells a user that the group has been left. + /// </summary> + GroupLeft, + /// <summary> + /// The group-wait update. Tells members of the group that a user is buffering. + /// </summary> + GroupWait, + /// <summary> + /// The prepare-session update. Tells a user to load some content. + /// </summary> + PrepareSession, + /// <summary> + /// The not-in-group error. Tells a user that they don't belong to a group. + /// </summary> + NotInGroup, + /// <summary> + /// The group-does-not-exist error. Sent when trying to join a non-existing group. + /// </summary> + GroupDoesNotExist, + /// <summary> + /// The create-group-denied error. Sent when a user tries to create a group without required permissions. + /// </summary> + CreateGroupDenied, + /// <summary> + /// The join-group-denied error. Sent when a user tries to join a group without required permissions. + /// </summary> + JoinGroupDenied, + /// <summary> + /// The library-access-denied error. Sent when a user tries to join a group without required access to the library. + /// </summary> + LibraryAccessDenied + } +} diff --git a/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs new file mode 100644 index 0000000000..d67b6bd555 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs @@ -0,0 +1,22 @@ +using System; + +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Class JoinGroupRequest. + /// </summary> + public class JoinGroupRequest + { + /// <summary> + /// Gets or sets the Group id. + /// </summary> + /// <value>The Group id to join.</value> + public Guid GroupId { get; set; } + + /// <summary> + /// Gets or sets the playing item id. + /// </summary> + /// <value>The client's currently playing item id.</value> + public Guid PlayingItemId { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs new file mode 100644 index 0000000000..9de23194e3 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs @@ -0,0 +1,34 @@ +using System; + +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Class PlaybackRequest. + /// </summary> + public class PlaybackRequest + { + /// <summary> + /// Gets or sets the request type. + /// </summary> + /// <value>The request type.</value> + public PlaybackRequestType Type { get; set; } + + /// <summary> + /// Gets or sets when the request has been made by the client. + /// </summary> + /// <value>The date of the request.</value> + public DateTime? When { get; set; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long? PositionTicks { get; set; } + + /// <summary> + /// Gets or sets the ping time. + /// </summary> + /// <value>The ping time.</value> + public long? Ping { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs new file mode 100644 index 0000000000..f1e175fdec --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs @@ -0,0 +1,33 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Enum PlaybackRequestType + /// </summary> + public enum PlaybackRequestType + { + /// <summary> + /// A user is requesting a play command for the group. + /// </summary> + Play = 0, + /// <summary> + /// A user is requesting a pause command for the group. + /// </summary> + Pause = 1, + /// <summary> + /// A user is requesting a seek command for the group. + /// </summary> + Seek = 2, + /// <summary> + /// A user is signaling that playback is buffering. + /// </summary> + Buffering = 3, + /// <summary> + /// A user is signaling that playback resumed. + /// </summary> + BufferingDone = 4, + /// <summary> + /// A user is reporting its ping. + /// </summary> + UpdatePing = 5 + } +} diff --git a/MediaBrowser.Model/SyncPlay/SendCommand.cs b/MediaBrowser.Model/SyncPlay/SendCommand.cs new file mode 100644 index 0000000000..0f06e381f1 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SendCommand.cs @@ -0,0 +1,38 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Class SendCommand. + /// </summary> + public class SendCommand + { + /// <summary> + /// Gets or sets the group identifier. + /// </summary> + /// <value>The group identifier.</value> + public string GroupId { get; set; } + + /// <summary> + /// Gets or sets the UTC time when to execute the command. + /// </summary> + /// <value>The UTC time when to execute the command.</value> + public string When { get; set; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long? PositionTicks { get; set; } + + /// <summary> + /// Gets or sets the command. + /// </summary> + /// <value>The command.</value> + public SendCommandType Command { get; set; } + + /// <summary> + /// Gets or sets the UTC time when this command has been emitted. + /// </summary> + /// <value>The UTC time when this command has been emitted.</value> + public string EmittedAt { get; set; } + } +} diff --git a/MediaBrowser.Model/SyncPlay/SendCommandType.cs b/MediaBrowser.Model/SyncPlay/SendCommandType.cs new file mode 100644 index 0000000000..1137198715 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SendCommandType.cs @@ -0,0 +1,21 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Enum SendCommandType. + /// </summary> + public enum SendCommandType + { + /// <summary> + /// The play command. Instructs users to start playback. + /// </summary> + Play = 0, + /// <summary> + /// The pause command. Instructs users to pause playback. + /// </summary> + Pause = 1, + /// <summary> + /// The seek command. Instructs users to seek to a specified time. + /// </summary> + Seek = 2 + } +} diff --git a/MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs b/MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs new file mode 100644 index 0000000000..0a60361544 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/UtcTimeResponse.cs @@ -0,0 +1,20 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Class UtcTimeResponse. + /// </summary> + public class UtcTimeResponse + { + /// <summary> + /// Gets or sets the UTC time when request has been received. + /// </summary> + /// <value>The UTC time when request has been received.</value> + public string RequestReceptionTime { get; set; } + + /// <summary> + /// Gets or sets the UTC time when response has been sent. + /// </summary> + /// <value>The UTC time when response has been sent.</value> + public string ResponseTransmissionTime { get; set; } + } +} diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index cfa7684c91..f2c5aa1e39 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -26,8 +26,6 @@ namespace MediaBrowser.Model.System /// </summary> public class SystemInfo : PublicSystemInfo { - public PackageVersionClass SystemUpdateLevel { get; set; } - /// <summary> /// Gets or sets the display name of the operating system. /// </summary> @@ -117,24 +115,6 @@ namespace MediaBrowser.Model.System /// <value>The transcode path.</value> public string TranscodingTempPath { get; set; } - /// <summary> - /// Gets or sets the HTTP server port number. - /// </summary> - /// <value>The HTTP server port number.</value> - public int HttpServerPortNumber { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [enable HTTPS]. - /// </summary> - /// <value><c>true</c> if [enable HTTPS]; otherwise, <c>false</c>.</value> - public bool SupportsHttps { get; set; } - - /// <summary> - /// Gets or sets the HTTPS server port number. - /// </summary> - /// <value>The HTTPS server port number.</value> - public int HttpsPortNumber { get; set; } - /// <summary> /// Gets or sets a value indicating whether this instance has update available. /// </summary> diff --git a/MediaBrowser.Model/Updates/CheckForUpdateResult.cs b/MediaBrowser.Model/Updates/CheckForUpdateResult.cs deleted file mode 100644 index be1b082238..0000000000 --- a/MediaBrowser.Model/Updates/CheckForUpdateResult.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace MediaBrowser.Model.Updates -{ - /// <summary> - /// Class CheckForUpdateResult. - /// </summary> - public class CheckForUpdateResult - { - /// <summary> - /// Gets or sets a value indicating whether this instance is update available. - /// </summary> - /// <value><c>true</c> if this instance is update available; otherwise, <c>false</c>.</value> - public bool IsUpdateAvailable { get; set; } - - /// <summary> - /// Gets or sets the available version. - /// </summary> - /// <value>The available version.</value> - public string AvailableVersion - { - get => Package != null ? Package.versionStr : "0.0.0.1"; - set { } // need this for the serializer - } - - /// <summary> - /// Get or sets package information for an available update - /// </summary> - public PackageVersionInfo Package { get; set; } - } -} diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs index 42c2105f54..e0d450d065 100644 --- a/MediaBrowser.Model/Updates/InstallationInfo.cs +++ b/MediaBrowser.Model/Updates/InstallationInfo.cs @@ -8,10 +8,10 @@ namespace MediaBrowser.Model.Updates public class InstallationInfo { /// <summary> - /// Gets or sets the id. + /// Gets or sets the guid. /// </summary> - /// <value>The id.</value> - public Guid Id { get; set; } + /// <value>The guid.</value> + public string Guid { get; set; } /// <summary> /// Gets or sets the name. @@ -19,22 +19,10 @@ namespace MediaBrowser.Model.Updates /// <value>The name.</value> public string Name { get; set; } - /// <summary> - /// Gets or sets the assembly guid. - /// </summary> - /// <value>The guid of the assembly.</value> - public string AssemblyGuid { get; set; } - /// <summary> /// Gets or sets the version. /// </summary> /// <value>The version.</value> public string Version { get; set; } - - /// <summary> - /// Gets or sets the update class. - /// </summary> - /// <value>The update class.</value> - public PackageVersionClass UpdateClass { get; set; } } } diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs index abbe91eff6..f5aa8b6fa2 100644 --- a/MediaBrowser.Model/Updates/PackageInfo.cs +++ b/MediaBrowser.Model/Updates/PackageInfo.cs @@ -8,12 +8,6 @@ namespace MediaBrowser.Model.Updates /// </summary> public class PackageInfo { - /// <summary> - /// The internal id of this package. - /// </summary> - /// <value>The id.</value> - public string id { get; set; } - /// <summary> /// Gets or sets the name. /// </summary> @@ -21,59 +15,17 @@ namespace MediaBrowser.Model.Updates public string name { get; set; } /// <summary> - /// Gets or sets the short description. + /// Gets or sets a long description of the plugin containing features or helpful explanations. /// </summary> - /// <value>The short description.</value> - public string shortDescription { get; set; } + /// <value>The description.</value> + public string description { get; set; } /// <summary> - /// Gets or sets the overview. + /// Gets or sets a short overview of what the plugin does. /// </summary> /// <value>The overview.</value> public string overview { get; set; } - /// <summary> - /// Gets or sets a value indicating whether this instance is premium. - /// </summary> - /// <value><c>true</c> if this instance is premium; otherwise, <c>false</c>.</value> - public bool isPremium { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is adult only content. - /// </summary> - /// <value><c>true</c> if this instance is adult; otherwise, <c>false</c>.</value> - public bool adult { get; set; } - - /// <summary> - /// Gets or sets the rich desc URL. - /// </summary> - /// <value>The rich desc URL.</value> - public string richDescUrl { get; set; } - - /// <summary> - /// Gets or sets the thumb image. - /// </summary> - /// <value>The thumb image.</value> - public string thumbImage { get; set; } - - /// <summary> - /// Gets or sets the preview image. - /// </summary> - /// <value>The preview image.</value> - public string previewImage { get; set; } - - /// <summary> - /// Gets or sets the type. - /// </summary> - /// <value>The type.</value> - public string type { get; set; } - - /// <summary> - /// Gets or sets the target filename. - /// </summary> - /// <value>The target filename.</value> - public string targetFilename { get; set; } - /// <summary> /// Gets or sets the owner. /// </summary> @@ -87,90 +39,24 @@ namespace MediaBrowser.Model.Updates public string category { get; set; } /// <summary> - /// Gets or sets the catalog tile color. - /// </summary> - /// <value>The owner.</value> - public string tileColor { get; set; } - - /// <summary> - /// Gets or sets the feature id of this package (if premium). - /// </summary> - /// <value>The feature id.</value> - public string featureId { get; set; } - - /// <summary> - /// Gets or sets the registration info for this package (if premium). - /// </summary> - /// <value>The registration info.</value> - public string regInfo { get; set; } - - /// <summary> - /// Gets or sets the price for this package (if premium). - /// </summary> - /// <value>The price.</value> - public float price { get; set; } - - /// <summary> - /// Gets or sets the target system for this plug-in (Server, MBTheater, MBClassic). - /// </summary> - /// <value>The target system.</value> - public PackageTargetSystem targetSystem { get; set; } - - /// <summary> - /// The guid of the assembly associated with this package (if a plug-in). + /// The guid of the assembly associated with this plugin. /// This is used to identify the proper item for automatic updates. /// </summary> /// <value>The name.</value> public string guid { get; set; } - /// <summary> - /// Gets or sets the total number of ratings for this package. - /// </summary> - /// <value>The total ratings.</value> - public int? totalRatings { get; set; } - - /// <summary> - /// Gets or sets the average rating for this package . - /// </summary> - /// <value>The rating.</value> - public float avgRating { get; set; } - - /// <summary> - /// Gets or sets whether or not this package is registered. - /// </summary> - /// <value>True if registered.</value> - public bool isRegistered { get; set; } - - /// <summary> - /// Gets or sets the expiration date for this package. - /// </summary> - /// <value>Expiration Date.</value> - public DateTime expDate { get; set; } - /// <summary> /// Gets or sets the versions. /// </summary> /// <value>The versions.</value> - public IReadOnlyList<PackageVersionInfo> versions { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [enable in application store]. - /// </summary> - /// <value><c>true</c> if [enable in application store]; otherwise, <c>false</c>.</value> - public bool enableInAppStore { get; set; } - - /// <summary> - /// Gets or sets the installs. - /// </summary> - /// <value>The installs.</value> - public int installs { get; set; } + public IReadOnlyList<VersionInfo> versions { get; set; } /// <summary> /// Initializes a new instance of the <see cref="PackageInfo"/> class. /// </summary> public PackageInfo() { - versions = Array.Empty<PackageVersionInfo>(); + versions = Array.Empty<VersionInfo>(); } } } diff --git a/MediaBrowser.Model/Updates/PackageTargetSystem.cs b/MediaBrowser.Model/Updates/PackageTargetSystem.cs deleted file mode 100644 index 11af7f02dd..0000000000 --- a/MediaBrowser.Model/Updates/PackageTargetSystem.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MediaBrowser.Model.Updates -{ - /// <summary> - /// Enum PackageType. - /// </summary> - public enum PackageTargetSystem - { - /// <summary> - /// Server. - /// </summary> - Server, - - /// <summary> - /// MB Theater. - /// </summary> - MBTheater, - - /// <summary> - /// MB Classic. - /// </summary> - MBClassic - } -} diff --git a/MediaBrowser.Model/Updates/PackageVersionClass.cs b/MediaBrowser.Model/Updates/PackageVersionClass.cs deleted file mode 100644 index f813f2c974..0000000000 --- a/MediaBrowser.Model/Updates/PackageVersionClass.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MediaBrowser.Model.Updates -{ - /// <summary> - /// Enum PackageVersionClass. - /// </summary> - public enum PackageVersionClass - { - /// <summary> - /// The release. - /// </summary> - Release = 0, - - /// <summary> - /// The beta. - /// </summary> - Beta = 1, - - /// <summary> - /// The dev. - /// </summary> - Dev = 2 - } -} diff --git a/MediaBrowser.Model/Updates/PackageVersionInfo.cs b/MediaBrowser.Model/Updates/PackageVersionInfo.cs deleted file mode 100644 index 3eef965dd6..0000000000 --- a/MediaBrowser.Model/Updates/PackageVersionInfo.cs +++ /dev/null @@ -1,96 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Model.Updates -{ - /// <summary> - /// Class PackageVersionInfo. - /// </summary> - public class PackageVersionInfo - { - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string name { get; set; } - - /// <summary> - /// Gets or sets the guid. - /// </summary> - /// <value>The guid.</value> - public string guid { get; set; } - - /// <summary> - /// Gets or sets the version STR. - /// </summary> - /// <value>The version STR.</value> - public string versionStr { get; set; } - - /// <summary> - /// The _version - /// </summary> - private Version _version; - - /// <summary> - /// Gets or sets the version. - /// Had to make this an interpreted property since Protobuf can't handle Version - /// </summary> - /// <value>The version.</value> - [JsonIgnore] - public Version Version - { - get - { - if (_version == null) - { - var ver = versionStr; - _version = new Version(string.IsNullOrEmpty(ver) ? "0.0.0.1" : ver); - } - - return _version; - } - } - - /// <summary> - /// Gets or sets the classification. - /// </summary> - /// <value>The classification.</value> - public PackageVersionClass classification { get; set; } - - /// <summary> - /// Gets or sets the description. - /// </summary> - /// <value>The description.</value> - public string description { get; set; } - - /// <summary> - /// Gets or sets the required version STR. - /// </summary> - /// <value>The required version STR.</value> - public string requiredVersionStr { get; set; } - - /// <summary> - /// Gets or sets the source URL. - /// </summary> - /// <value>The source URL.</value> - public string sourceUrl { get; set; } - - /// <summary> - /// Gets or sets the source URL. - /// </summary> - /// <value>The source URL.</value> - public string checksum { get; set; } - - /// <summary> - /// Gets or sets the target filename. - /// </summary> - /// <value>The target filename.</value> - public string targetFilename { get; set; } - - public string infoUrl { get; set; } - - public string runtimes { get; set; } - } -} diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs new file mode 100644 index 0000000000..fe5826ad2f --- /dev/null +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -0,0 +1,58 @@ +using System; + +namespace MediaBrowser.Model.Updates +{ + /// <summary> + /// Class PackageVersionInfo. + /// </summary> + public class VersionInfo + { + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string name { get; set; } + + /// <summary> + /// Gets or sets the guid. + /// </summary> + /// <value>The guid.</value> + public string guid { get; set; } + + /// <summary> + /// Gets or sets the version. + /// </summary> + /// <value>The version.</value> + public Version version { get; set; } + + /// <summary> + /// Gets or sets the changelog for this version. + /// </summary> + /// <value>The changelog.</value> + public string changelog { get; set; } + + /// <summary> + /// Gets or sets the ABI that this version was built against. + /// </summary> + /// <value>The target ABI version.</value> + public string targetAbi { get; set; } + + /// <summary> + /// Gets or sets the source URL. + /// </summary> + /// <value>The source URL.</value> + public string sourceUrl { get; set; } + + /// <summary> + /// Gets or sets a checksum for the binary. + /// </summary> + /// <value>The checksum.</value> + public string checksum { get; set; } + + /// <summary> + /// Gets or sets the target filename for the downloaded binary. + /// </summary> + /// <value>The target filename.</value> + public string filename { get; set; } + } +} diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index ae2b3fd4e9..3e027e8312 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -80,6 +80,12 @@ namespace MediaBrowser.Model.Users public string AuthenticationProviderId { get; set; } public string PasswordResetProviderId { get; set; } + /// <summary> + /// Gets or sets a value indicating what SyncPlay features the user can access. + /// </summary> + /// <value>Access level to SyncPlay features.</value> + public SyncPlayAccess SyncPlayAccess { get; set; } + public UserPolicy() { IsHidden = true; @@ -125,6 +131,7 @@ namespace MediaBrowser.Model.Users EnableContentDownloading = true; EnablePublicSharing = true; EnableRemoteAccess = true; + SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups; } } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 7125f34c55..cfff897672 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -36,60 +36,51 @@ namespace MediaBrowser.Providers.Manager /// </summary> public class ProviderManager : IProviderManager, IDisposable { - /// <summary> - /// The _logger - /// </summary> private readonly ILogger _logger; - - /// <summary> - /// The _HTTP client - /// </summary> private readonly IHttpClient _httpClient; - - /// <summary> - /// The _directory watchers - /// </summary> private readonly ILibraryMonitor _libraryMonitor; - - /// <summary> - /// Gets or sets the configuration manager. - /// </summary> - /// <value>The configuration manager.</value> - private IServerConfigurationManager ConfigurationManager { get; set; } + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationPaths _appPaths; + private readonly IJsonSerializer _json; + private readonly ILibraryManager _libraryManager; + private readonly ISubtitleManager _subtitleManager; + private readonly IServerConfigurationManager _configurationManager; private IImageProvider[] ImageProviders { get; set; } - private readonly IFileSystem _fileSystem; - private IMetadataService[] _metadataServices = { }; private IMetadataProvider[] _metadataProviders = { }; private IEnumerable<IMetadataSaver> _savers; - private readonly IServerApplicationPaths _appPaths; - private readonly IJsonSerializer _json; private IExternalId[] _externalIds; - private readonly Func<ILibraryManager> _libraryManagerFactory; private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted; public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted; public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress; - private ISubtitleManager _subtitleManager; - /// <summary> /// Initializes a new instance of the <see cref="ProviderManager" /> class. /// </summary> - public ProviderManager(IHttpClient httpClient, ISubtitleManager subtitleManager, IServerConfigurationManager configurationManager, ILibraryMonitor libraryMonitor, ILoggerFactory loggerFactory, IFileSystem fileSystem, IServerApplicationPaths appPaths, Func<ILibraryManager> libraryManagerFactory, IJsonSerializer json) + public ProviderManager( + IHttpClient httpClient, + ISubtitleManager subtitleManager, + IServerConfigurationManager configurationManager, + ILibraryMonitor libraryMonitor, + ILogger<ProviderManager> logger, + IFileSystem fileSystem, + IServerApplicationPaths appPaths, + ILibraryManager libraryManager, + IJsonSerializer json) { - _logger = loggerFactory.CreateLogger("ProviderManager"); + _logger = logger; _httpClient = httpClient; - ConfigurationManager = configurationManager; + _configurationManager = configurationManager; _libraryMonitor = libraryMonitor; _fileSystem = fileSystem; _appPaths = appPaths; - _libraryManagerFactory = libraryManagerFactory; + _libraryManager = libraryManager; _json = json; _subtitleManager = subtitleManager; } @@ -176,7 +167,7 @@ namespace MediaBrowser.Providers.Manager public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken) { - return new ImageSaver(ConfigurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken); + return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken); } public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken) @@ -188,7 +179,7 @@ namespace MediaBrowser.Providers.Manager var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, true); - return new ImageSaver(ConfigurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken); + return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken); } public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken) @@ -273,7 +264,7 @@ namespace MediaBrowser.Providers.Manager public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions) { - return GetImageProviders(item, _libraryManagerFactory().GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false); + return GetImageProviders(item, _libraryManager.GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false); } private IEnumerable<IImageProvider> GetImageProviders(BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, ImageRefreshOptions refreshOptions, bool includeDisabled) @@ -328,7 +319,7 @@ namespace MediaBrowser.Providers.Manager private IEnumerable<IRemoteImageProvider> GetRemoteImageProviders(BaseItem item, bool includeDisabled) { var options = GetMetadataOptions(item); - var libraryOptions = _libraryManagerFactory().GetLibraryOptions(item); + var libraryOptions = _libraryManager.GetLibraryOptions(item); return GetImageProviders(item, libraryOptions, options, new ImageRefreshOptions( @@ -593,7 +584,7 @@ namespace MediaBrowser.Providers.Manager { var type = item.GetType().Name; - return ConfigurationManager.Configuration.MetadataOptions + return _configurationManager.Configuration.MetadataOptions .FirstOrDefault(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) ?? new MetadataOptions(); } @@ -623,7 +614,7 @@ namespace MediaBrowser.Providers.Manager /// <returns>Task.</returns> private void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers) { - var libraryOptions = _libraryManagerFactory().GetLibraryOptions(item); + var libraryOptions = _libraryManager.GetLibraryOptions(item); foreach (var saver in savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false))) { @@ -743,7 +734,7 @@ namespace MediaBrowser.Providers.Manager if (!searchInfo.ItemId.Equals(Guid.Empty)) { - referenceItem = _libraryManagerFactory().GetItemById(searchInfo.ItemId); + referenceItem = _libraryManager.GetItemById(searchInfo.ItemId); } return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken); @@ -771,7 +762,7 @@ namespace MediaBrowser.Providers.Manager } else { - libraryOptions = _libraryManagerFactory().GetLibraryOptions(referenceItem); + libraryOptions = _libraryManager.GetLibraryOptions(referenceItem); } var options = GetMetadataOptions(referenceItem); @@ -786,11 +777,11 @@ namespace MediaBrowser.Providers.Manager if (string.IsNullOrWhiteSpace(searchInfo.SearchInfo.MetadataLanguage)) { - searchInfo.SearchInfo.MetadataLanguage = ConfigurationManager.Configuration.PreferredMetadataLanguage; + searchInfo.SearchInfo.MetadataLanguage = _configurationManager.Configuration.PreferredMetadataLanguage; } if (string.IsNullOrWhiteSpace(searchInfo.SearchInfo.MetadataCountryCode)) { - searchInfo.SearchInfo.MetadataCountryCode = ConfigurationManager.Configuration.MetadataCountryCode; + searchInfo.SearchInfo.MetadataCountryCode = _configurationManager.Configuration.MetadataCountryCode; } var resultList = new List<RemoteSearchResult>(); @@ -967,7 +958,7 @@ namespace MediaBrowser.Providers.Manager public void OnRefreshProgress(BaseItem item, double progress) { var id = item.Id; - _logger.LogInformation("OnRefreshProgress {0} {1}", id.ToString("N", CultureInfo.InvariantCulture), progress); + _logger.LogDebug("OnRefreshProgress {0} {1}", id.ToString("N", CultureInfo.InvariantCulture), progress); // TODO: Need to hunt down the conditions for this happening _activeRefreshes.AddOrUpdate( @@ -1010,7 +1001,7 @@ namespace MediaBrowser.Providers.Manager private async Task StartProcessingRefreshQueue() { - var libraryManager = _libraryManagerFactory(); + var libraryManager = _libraryManager; if (_disposed) { @@ -1088,7 +1079,7 @@ namespace MediaBrowser.Providers.Manager private async Task RefreshArtist(MusicArtist item, MetadataRefreshOptions options, CancellationToken cancellationToken) { - var albums = _libraryManagerFactory() + var albums = _libraryManager .GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 330a4d1e53..5073b40157 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{442B5058-DCAF-4263-BB6A-F21E31120A1B}</ProjectGuid> + </PropertyGroup> + <ItemGroup> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> @@ -11,8 +16,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> - <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" /> + <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.4" /> <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> <PackageReference Include="PlaylistsNET" Version="1.0.4" /> <PackageReference Include="TvDbSharper" Version="3.0.1" /> diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index 3999025d8e..6982568eb2 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -46,7 +46,6 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IApplicationPaths _appPaths; private readonly IJsonSerializer _json; private readonly IEncodingManager _encodingManager; - private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; private readonly IChapterManager _chapterManager; @@ -134,7 +133,6 @@ namespace MediaBrowser.Providers.MediaInfo IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, - IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager, @@ -149,7 +147,6 @@ namespace MediaBrowser.Providers.MediaInfo _appPaths = appPaths; _json = json; _encodingManager = encodingManager; - _fileSystem = fileSystem; _config = config; _subtitleManager = subtitleManager; _chapterManager = chapterManager; @@ -157,7 +154,7 @@ namespace MediaBrowser.Providers.MediaInfo _channelManager = channelManager; _mediaSourceManager = mediaSourceManager; - _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager, fileSystem); + _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager); } private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None); @@ -202,7 +199,6 @@ namespace MediaBrowser.Providers.MediaInfo _blurayExaminer, _localization, _encodingManager, - _fileSystem, _config, _subtitleManager, _chapterManager, diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index d2e98a5a94..89496622fc 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -39,7 +39,6 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IBlurayExaminer _blurayExaminer; private readonly ILocalizationManager _localization; private readonly IEncodingManager _encodingManager; - private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; private readonly IChapterManager _chapterManager; @@ -56,7 +55,6 @@ namespace MediaBrowser.Providers.MediaInfo IBlurayExaminer blurayExaminer, ILocalizationManager localization, IEncodingManager encodingManager, - IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager, @@ -68,7 +66,6 @@ namespace MediaBrowser.Providers.MediaInfo _blurayExaminer = blurayExaminer; _localization = localization; _encodingManager = encodingManager; - _fileSystem = fileSystem; _config = config; _subtitleManager = subtitleManager; _chapterManager = chapterManager; @@ -76,7 +73,8 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; } - public async Task<ItemUpdateType> ProbeVideo<T>(T item, + public async Task<ItemUpdateType> ProbeVideo<T>( + T item, MetadataRefreshOptions options, CancellationToken cancellationToken) where T : Video @@ -99,7 +97,6 @@ namespace MediaBrowser.Providers.MediaInfo return ItemUpdateType.MetadataImport; } } - else if (item.VideoType == VideoType.BluRay) { var inputPath = item.Path; @@ -130,7 +127,8 @@ namespace MediaBrowser.Providers.MediaInfo return ItemUpdateType.MetadataImport; } - private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(Video item, + private Task<Model.MediaInfo.MediaInfo> GetMediaInfo( + Video item, string[] streamFileNames, CancellationToken cancellationToken) { @@ -145,22 +143,24 @@ namespace MediaBrowser.Providers.MediaInfo protocol = _mediaSourceManager.GetPathProtocol(path); } - return _mediaEncoder.GetMediaInfo(new MediaInfoRequest - { - PlayableStreamFileNames = streamFileNames, - ExtractChapters = true, - MediaType = DlnaProfileType.Video, - MediaSource = new MediaSourceInfo + return _mediaEncoder.GetMediaInfo( + new MediaInfoRequest { - Path = path, - Protocol = protocol, - VideoType = item.VideoType - } - - }, cancellationToken); + PlayableStreamFileNames = streamFileNames, + ExtractChapters = true, + MediaType = DlnaProfileType.Video, + MediaSource = new MediaSourceInfo + { + Path = path, + Protocol = protocol, + VideoType = item.VideoType + } + }, + cancellationToken); } - protected async Task Fetch(Video video, + protected async Task Fetch( + Video video, CancellationToken cancellationToken, Model.MediaInfo.MediaInfo mediaInfo, BlurayDiscInfo blurayInfo, @@ -491,12 +491,13 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="options">The refreshOptions.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private async Task AddExternalSubtitles(Video video, + private async Task AddExternalSubtitles( + Video video, List<MediaStream> currentStreams, MetadataRefreshOptions options, CancellationToken cancellationToken) { - var subtitleResolver = new SubtitleResolver(_localization, _fileSystem); + var subtitleResolver = new SubtitleResolver(_localization); var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1); var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, false); @@ -605,7 +606,7 @@ namespace MediaBrowser.Providers.MediaInfo private string[] FetchFromDvdLib(Video item) { var path = item.Path; - var dvd = new Dvd(path, _fileSystem); + var dvd = new Dvd(path); var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault(); diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index 7ebbb9e237..2bbe8a968d 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -13,7 +13,6 @@ namespace MediaBrowser.Providers.MediaInfo public class SubtitleResolver { private readonly ILocalizationManager _localization; - private readonly IFileSystem _fileSystem; private static readonly HashSet<string> SubtitleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { @@ -26,16 +25,16 @@ namespace MediaBrowser.Providers.MediaInfo ".vtt" }; - public SubtitleResolver(ILocalizationManager localization, IFileSystem fileSystem) + public SubtitleResolver(ILocalizationManager localization) { _localization = localization; - _fileSystem = fileSystem; } - public List<MediaStream> GetExternalSubtitleStreams(Video video, - int startIndex, - IDirectoryService directoryService, - bool clearCache) + public List<MediaStream> GetExternalSubtitleStreams( + Video video, + int startIndex, + IDirectoryService directoryService, + bool clearCache) { var streams = new List<MediaStream>(); diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html index 34494644d4..fbf413f2b5 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -31,8 +31,8 @@ $('.configPage').on('pageshow', function () { Dashboard.showLoadingMsg(); ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - $('#enable').checked(config.Enable); - $('#replaceAlbumName').checked(config.ReplaceAlbumName); + $('#enable').checked = config.Enable; + $('#replaceAlbumName').checked = config.ReplaceAlbumName; Dashboard.hideLoadingMsg(); }); @@ -43,8 +43,8 @@ var form = this; ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - config.Enable = $('#enable', form).checked(); - config.ReplaceAlbumName = $('#replaceAlbumName', form).checked(); + config.Enable = $('#enable', form).checked; + config.ReplaceAlbumName = $('#replaceAlbumName', form).checked; ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 1f02461da2..90196b046b 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -41,8 +41,8 @@ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { $('#server').val(config.Server).change(); $('#rateLimit').val(config.RateLimit).change(); - $('#enable').checked(config.Enable); - $('#replaceArtistName').checked(config.ReplaceArtistName); + $('#enable').checked = config.Enable; + $('#replaceArtistName').checked = config.ReplaceArtistName; Dashboard.hideLoadingMsg(); }); @@ -55,8 +55,8 @@ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { config.Server = $('#server', form).val(); config.RateLimit = $('#rateLimit', form).val(); - config.Enable = $('#enable', form).checked(); - config.ReplaceArtistName = $('#replaceArtistName', form).checked(); + config.Enable = $('#enable', form).checked; + config.ReplaceArtistName = $('#replaceArtistName', form).checked; ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs index 37160dd2c0..f0328e8d87 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs @@ -11,13 +11,10 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Omdb { - public class OmdbEpisodeProvider : - IRemoteMetadataProvider<Episode, EpisodeInfo>, - IHasOrder + public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder { private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; @@ -26,16 +23,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb private readonly IServerConfigurationManager _configurationManager; private readonly IApplicationHost _appHost; - public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager) + public OmdbEpisodeProvider( + IJsonSerializer jsonSerializer, + IApplicationHost appHost, + IHttpClient httpClient, + ILibraryManager libraryManager, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; - _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, logger, libraryManager, fileSystem, configurationManager); + _itemProvider = new OmdbItemProvider(jsonSerializer, _appHost, httpClient, libraryManager, fileSystem, configurationManager); } + // After TheTvDb + public int Order => 1; + + public string Name => "The Open Movie Database"; + public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) { return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken); @@ -66,10 +74,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb return result; } - // After TheTvDb - public int Order => 1; - - public string Name => "The Open Movie Database"; public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 3aadda5d07..64a75955a2 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -17,7 +17,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Omdb { @@ -26,22 +25,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb { private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; - private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IApplicationHost _appHost; - public OmdbItemProvider(IJsonSerializer jsonSerializer, IApplicationHost appHost, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager configurationManager) + public OmdbItemProvider( + IJsonSerializer jsonSerializer, + IApplicationHost appHost, + IHttpClient httpClient, + ILibraryManager libraryManager, + IFileSystem fileSystem, + IServerConfigurationManager configurationManager) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; - _logger = logger; _libraryManager = libraryManager; _fileSystem = fileSystem; _configurationManager = configurationManager; _appHost = appHost; } + // After primary option public int Order => 2; @@ -80,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var parsedName = _libraryManager.ParseName(name); var yearInName = parsedName.Year; name = parsedName.Name; - year = year ?? yearInName; + year ??= yearInName; } if (string.IsNullOrWhiteSpace(imdbId)) @@ -312,6 +316,5 @@ namespace MediaBrowser.Providers.Plugins.Omdb /// <value>The results.</value> public List<SearchResult> Search { get; set; } } - } } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 583c7e8ea4..127d29c04e 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -25,22 +25,22 @@ namespace MediaBrowser.Providers.Subtitles { public class SubtitleManager : ISubtitleManager { - private ISubtitleProvider[] _subtitleProviders; private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _monitor; private readonly IMediaSourceManager _mediaSourceManager; + private readonly ILocalizationManager _localization; - private ILocalizationManager _localization; + private ISubtitleProvider[] _subtitleProviders; public SubtitleManager( - ILoggerFactory loggerFactory, + ILogger<SubtitleManager> logger, IFileSystem fileSystem, ILibraryMonitor monitor, IMediaSourceManager mediaSourceManager, ILocalizationManager localizationManager) { - _logger = loggerFactory.CreateLogger(nameof(SubtitleManager)); + _logger = logger; _fileSystem = fileSystem; _monitor = monitor; _mediaSourceManager = mediaSourceManager; diff --git a/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs index 223cef086b..bf63946084 100644 --- a/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using System.Text.RegularExpressions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -18,8 +19,22 @@ namespace MediaBrowser.Providers.Tmdb.Movies { public class TmdbSearch { - private static readonly CultureInfo EnUs = new CultureInfo("en-US"); - private const string Search3 = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; + private static readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled); + private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled); + private static readonly Regex _cleanStopWords = new Regex(@"\b( # Start at word boundary + 19[0-9]{2}|20[0-9]{2}| # 1900-2099 + S[0-9]{2}| # Season + E[0-9]{2}| # Episode + (2160|1080|720|576|480)[ip]?| # Resolution + [xh]?264| # Encoding + (web|dvd|bd|hdtv|hd)rip| # *Rip + web|hdtv|mp4|bluray|ktr|dl|single|imageset|internal|doku|dubbed|retail|xxx|flac + ).* # Match rest of string", + RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase); + + private const string _searchURL = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; private readonly ILogger _logger; private readonly IJsonSerializer _json; @@ -61,19 +76,18 @@ namespace MediaBrowser.Providers.Tmdb.Movies var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - if (!string.IsNullOrWhiteSpace(name)) - { - var parsedName = _libraryManager.ParseName(name); - var yearInName = parsedName.Year; - name = parsedName.Name; - year = year ?? yearInName; - } + // TODO: Investigate: Does this mean we are reparsing already parsed ItemLookupInfo? + var parsedName = _libraryManager.ParseName(name); + var yearInName = parsedName.Year; + name = parsedName.Name; + year ??= yearInName; - _logger.LogInformation("MovieDbProvider: Finding id for item: " + name); + _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name, year); var language = idInfo.MetadataLanguage.ToLowerInvariant(); - //nope - search for it - //var searchType = item is BoxSet ? "collection" : "movie"; + // Replace sequences of non-word characters with space + // TMDB expects a space separated list of words make sure that is the case + name = _cleanNonWord.Replace(name, " ").Trim(); var results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); @@ -86,36 +100,35 @@ namespace MediaBrowser.Providers.Tmdb.Movies } } + // TODO: retrying alternatives should be done outside the search + // provider so that the retry logic can be common for all search + // providers if (results.Count == 0) { - // try with dot and _ turned to space - var originalName = name; - - name = name.Replace(",", " "); - name = name.Replace(".", " "); - name = name.Replace("_", " "); - name = name.Replace("-", " "); - name = name.Replace("!", " "); - name = name.Replace("?", " "); - - var parenthIndex = name.IndexOf('('); - if (parenthIndex != -1) - { - name = name.Substring(0, parenthIndex); - } + var name2 = parsedName.Name; - name = name.Trim(); + // Remove things enclosed in []{}() etc + name2 = _cleanEnclosed.Replace(name2, string.Empty); + + // Replace sequences of non-word characters with space + name2 = _cleanNonWord.Replace(name2, " "); + + // Clean based on common stop words / tokens + name2 = _cleanStopWords.Replace(name2, string.Empty); + + // Trim whitespace + name2 = name2.Trim(); // Search again if the new name is different - if (!string.Equals(name, originalName)) + if (!string.Equals(name2, name) && !string.IsNullOrWhiteSpace(name2)) { - results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); + _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year); + results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); if (results.Count == 0 && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) { //one more time, in english - results = await GetSearchResults(name, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false); - + results = await GetSearchResults(name2, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false); } } } @@ -150,7 +163,7 @@ namespace MediaBrowser.Providers.Tmdb.Movies throw new ArgumentException("name"); } - var url3 = string.Format(Search3, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type); + var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type); using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { @@ -179,14 +192,14 @@ namespace MediaBrowser.Providers.Tmdb.Movies if (!string.IsNullOrWhiteSpace(i.Release_Date)) { // These dates are always in this exact format - if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r)) + if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r)) { remoteResult.PremiereDate = r.ToUniversalTime(); remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; } } - remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs)); + remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture)); return remoteResult; @@ -203,7 +216,7 @@ namespace MediaBrowser.Providers.Tmdb.Movies throw new ArgumentException("name"); } - var url3 = string.Format(Search3, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv"); + var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv"); using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { @@ -232,14 +245,14 @@ namespace MediaBrowser.Providers.Tmdb.Movies if (!string.IsNullOrWhiteSpace(i.First_Air_Date)) { // These dates are always in this exact format - if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r)) + if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r)) { remoteResult.PremiereDate = r.ToUniversalTime(); remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; } } - remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs)); + remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture)); return remoteResult; diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs index 7195dc42a7..6e3c26c263 100644 --- a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs @@ -27,9 +27,6 @@ namespace MediaBrowser.Providers.Tmdb.TV public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder { private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings"; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - internal static TmdbSeriesProvider Current { get; private set; } private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; @@ -39,6 +36,10 @@ namespace MediaBrowser.Providers.Tmdb.TV private readonly IHttpClient _httpClient; private readonly ILibraryManager _libraryManager; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + internal static TmdbSeriesProvider Current { get; private set; } + public TmdbSeriesProvider( IJsonSerializer jsonSerializer, IFileSystem fileSystem, @@ -217,10 +218,9 @@ namespace MediaBrowser.Providers.Tmdb.TV var series = seriesResult.Item; series.Name = seriesInfo.Name; + series.OriginalTitle = seriesInfo.Original_Name; series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.Id.ToString(_usCulture)); - //series.VoteCount = seriesInfo.vote_count; - string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture); if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating)) @@ -240,7 +240,7 @@ namespace MediaBrowser.Providers.Tmdb.TV series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray(); } - //series.HomePageUrl = seriesInfo.homepage; + series.HomePageUrl = seriesInfo.Homepage; series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); @@ -308,29 +308,61 @@ namespace MediaBrowser.Providers.Tmdb.TV seriesResult.ResetPeople(); var tmdbImageUrl = settings.images.GetImageUrl("original"); - if (seriesInfo.Credits != null && seriesInfo.Credits.Cast != null) + if (seriesInfo.Credits != null) { - foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order)) + if (seriesInfo.Credits.Cast != null) { - var personInfo = new PersonInfo + foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order)) { - Name = actor.Name.Trim(), - Role = actor.Character, - Type = PersonType.Actor, - SortOrder = actor.Order - }; + var personInfo = new PersonInfo + { + Name = actor.Name.Trim(), + Role = actor.Character, + Type = PersonType.Actor, + SortOrder = actor.Order + }; - if (!string.IsNullOrWhiteSpace(actor.Profile_Path)) - { - personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path; + if (!string.IsNullOrWhiteSpace(actor.Profile_Path)) + { + personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path; + } + + if (actor.Id > 0) + { + personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); + } + + seriesResult.AddPerson(personInfo); } + } - if (actor.Id > 0) + if (seriesInfo.Credits.Crew != null) + { + var keepTypes = new[] { - personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); - } + PersonType.Director, + PersonType.Writer, + PersonType.Producer + }; - seriesResult.AddPerson(personInfo); + foreach (var person in seriesInfo.Credits.Crew) + { + // Normalize this + var type = TmdbUtils.MapCrewToPersonType(person); + + if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) + && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + seriesResult.AddPerson(new PersonInfo + { + Name = person.Name.Trim(), + Role = person.Job, + Type = type + }); + } } } } diff --git a/MediaBrowser.Providers/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Tmdb/TmdbUtils.cs index 035b99c1a6..7dacc74044 100644 --- a/MediaBrowser.Providers/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Tmdb/TmdbUtils.cs @@ -4,18 +4,51 @@ using MediaBrowser.Providers.Tmdb.Models.General; namespace MediaBrowser.Providers.Tmdb { + /// <summary> + /// Utilities for the TMDb provider + /// </summary> public static class TmdbUtils { + /// <summary> + /// URL of the TMDB instance to use. + /// </summary> public const string BaseTmdbUrl = "https://www.themoviedb.org/"; + + /// <summary> + /// URL of the TMDB API instance to use. + /// </summary> public const string BaseTmdbApiUrl = "https://api.themoviedb.org/"; + + /// <summary> + /// Name of the provider. + /// </summary> public const string ProviderName = "TheMovieDb"; + + /// <summary> + /// API key to use when performing an API call. + /// </summary> public const string ApiKey = "4219e299c89411838049ab0dab19ebd5"; + + /// <summary> + /// Value of the Accept header for requests to the provider. + /// </summary> public const string AcceptHeader = "application/json,image/*"; + /// <summary> + /// Maps the TMDB provided roles for crew members to Jellyfin roles. + /// </summary> + /// <param name="crew">Crew member to map against the Jellyfin person types.</param> + /// <returns>The Jellyfin person type.</returns> public static string MapCrewToPersonType(Crew crew) { if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase) - && crew.Job.IndexOf("producer", StringComparison.InvariantCultureIgnoreCase) != -1) + && crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase)) + { + return PersonType.Director; + } + + if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase) + && crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase)) { return PersonType.Producer; } diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 133a35527d..55fc463d0e 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -52,12 +52,6 @@ namespace MediaBrowser.WebDashboard.Api public string Name { get; set; } } - [Route("/web/Package", "GET", IsHidden = true)] - public class GetDashboardPackage - { - public string Mode { get; set; } - } - [Route("/robots.txt", "GET", IsHidden = true)] public class GetRobotsTxt { @@ -225,7 +219,7 @@ namespace MediaBrowser.WebDashboard.Api return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); } - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => PackageCreator.ModifyHtml(false, stream, null, _appHost.ApplicationVersionString, null)); + return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); } throw new ResourceNotFoundException(); @@ -328,154 +322,19 @@ namespace MediaBrowser.WebDashboard.Api throw new ResourceNotFoundException(); } - var path = request.ResourceName; - - var contentType = MimeTypes.GetMimeType(path); + var path = request?.ResourceName; var basePath = DashboardUIPath; // Bounce them to the startup wizard if it hasn't been completed yet - if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted && - Request.RawUrl.IndexOf("wizard", StringComparison.OrdinalIgnoreCase) == -1 && - PackageCreator.IsCoreHtml(path)) - { - // But don't redirect if an html import is being requested. - if (path.IndexOf("bower_components", StringComparison.OrdinalIgnoreCase) == -1) - { - Request.Response.Redirect("index.html?start=wizard#!/wizardstart.html"); - return null; - } - } - - var localizationCulture = GetLocalizationCulture(); - - // Don't cache if not configured to do so - // But always cache images to simulate production - if (!_serverConfigurationManager.Configuration.EnableDashboardResponseCaching && - !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) && - !contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase)) - { - var stream = await GetResourceStream(basePath, path, localizationCulture).ConfigureAwait(false); - return _resultFactory.GetResult(Request, stream, contentType); - } - - TimeSpan? cacheDuration = null; - - // Cache images unconditionally - updates to image files will require new filename - // If there's a version number in the query string we can cache this unconditionally - if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(request.V)) + if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted + && !Request.RawUrl.Contains("wizard", StringComparison.OrdinalIgnoreCase) + && Request.RawUrl.Contains("index", StringComparison.OrdinalIgnoreCase)) { - cacheDuration = TimeSpan.FromDays(365); - } - - var cacheKey = (_appHost.ApplicationVersionString + (localizationCulture ?? string.Empty) + path).GetMD5(); - - // html gets modified on the fly - if (contentType.StartsWith("text/html", StringComparison.OrdinalIgnoreCase)) - { - return await _resultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(basePath, path, localizationCulture)).ConfigureAwait(false); + Request.Response.Redirect("index.html?start=wizard#!/wizardstart.html"); + return null; } return await _resultFactory.GetStaticFileResult(Request, _resourceFileManager.GetResourcePath(basePath, path)).ConfigureAwait(false); } - - private string GetLocalizationCulture() - { - return _serverConfigurationManager.Configuration.UICulture; - } - - /// <summary> - /// Gets the resource stream. - /// </summary> - private Task<Stream> GetResourceStream(string basePath, string virtualPath, string localizationCulture) - { - return GetPackageCreator(basePath) - .GetResource(virtualPath, null, localizationCulture, _appHost.ApplicationVersionString); - } - - private PackageCreator GetPackageCreator(string basePath) - { - return new PackageCreator(basePath, _resourceFileManager); - } - - public async Task<object> Get(GetDashboardPackage request) - { - if (!_appConfig.HostWebClient() || DashboardUIPath == null) - { - throw new ResourceNotFoundException(); - } - - var mode = request.Mode; - - var inputPath = string.IsNullOrWhiteSpace(mode) ? - DashboardUIPath - : "C:\\dev\\emby-web-mobile-master\\dist"; - - var targetPath = !string.IsNullOrWhiteSpace(mode) ? - inputPath - : "C:\\dev\\emby-web-mobile\\src"; - - var packageCreator = GetPackageCreator(inputPath); - - if (!string.Equals(inputPath, targetPath, StringComparison.OrdinalIgnoreCase)) - { - try - { - Directory.Delete(targetPath, true); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting {Path}", targetPath); - } - - CopyDirectory(inputPath, targetPath); - } - - var appVersion = _appHost.ApplicationVersionString; - - await DumpHtml(packageCreator, inputPath, targetPath, mode, appVersion).ConfigureAwait(false); - - return string.Empty; - } - - private async Task DumpHtml(PackageCreator packageCreator, string source, string destination, string mode, string appVersion) - { - foreach (var file in _fileSystem.GetFiles(source)) - { - var filename = file.Name; - - if (!string.Equals(file.Extension, ".html", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - await DumpFile(packageCreator, filename, Path.Combine(destination, filename), mode, appVersion).ConfigureAwait(false); - } - } - - private async Task DumpFile(PackageCreator packageCreator, string resourceVirtualPath, string destinationFilePath, string mode, string appVersion) - { - using (var stream = await packageCreator.GetResource(resourceVirtualPath, mode, null, appVersion).ConfigureAwait(false)) - using (var fs = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - await stream.CopyToAsync(fs).ConfigureAwait(false); - } - } - - private void CopyDirectory(string source, string destination) - { - Directory.CreateDirectory(destination); - - // Now Create all of the directories - foreach (var dirPath in _fileSystem.GetDirectories(source, true)) - { - Directory.CreateDirectory(dirPath.FullName.Replace(source, destination, StringComparison.Ordinal)); - } - - // Copy all the files & Replaces any files with the same name - foreach (var newPath in _fileSystem.GetFiles(source, true)) - { - File.Copy(newPath.FullName, newPath.FullName.Replace(source, destination, StringComparison.Ordinal), true); - } - } } } diff --git a/MediaBrowser.WebDashboard/Api/PackageCreator.cs b/MediaBrowser.WebDashboard/Api/PackageCreator.cs deleted file mode 100644 index b7c15a840e..0000000000 --- a/MediaBrowser.WebDashboard/Api/PackageCreator.cs +++ /dev/null @@ -1,161 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Controller; - -namespace MediaBrowser.WebDashboard.Api -{ - public class PackageCreator - { - private readonly string _basePath; - private readonly IResourceFileManager _resourceFileManager; - - public PackageCreator(string basePath, IResourceFileManager resourceFileManager) - { - _basePath = basePath; - _resourceFileManager = resourceFileManager; - } - - public async Task<Stream> GetResource( - string virtualPath, - string mode, - string localizationCulture, - string appVersion) - { - var resourcePath = _resourceFileManager.GetResourcePath(_basePath, virtualPath); - Stream resourceStream = File.OpenRead(resourcePath); - - if (resourceStream != null && IsCoreHtml(virtualPath)) - { - bool isMainIndexPage = string.Equals(virtualPath, "index.html", StringComparison.OrdinalIgnoreCase); - resourceStream = await ModifyHtml(isMainIndexPage, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false); - } - - return resourceStream; - } - - public static bool IsCoreHtml(string path) - { - if (path.IndexOf(".template.html", StringComparison.OrdinalIgnoreCase) != -1) - { - return false; - } - - return string.Equals(Path.GetExtension(path), ".html", StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Modifies the source HTML stream by adding common meta tags, css and js. - /// </summary> - /// <param name="isMainIndexPage">True if the stream contains content for the main index page.</param> - /// <param name="sourceStream">The stream whose content should be modified.</param> - /// <param name="mode">The client mode ('cordova', 'android', etc).</param> - /// <param name="appVersion">The application version.</param> - /// <param name="localizationCulture">The localization culture.</param> - /// <returns> - /// A task that represents the async operation to read and modify the input stream. - /// The task result contains a stream containing the modified HTML content. - /// </returns> - public static async Task<Stream> ModifyHtml( - bool isMainIndexPage, - Stream sourceStream, - string mode, - string appVersion, - string localizationCulture) - { - string html; - using (var reader = new StreamReader(sourceStream, Encoding.UTF8)) - { - html = await reader.ReadToEndAsync().ConfigureAwait(false); - } - - if (isMainIndexPage && !string.IsNullOrWhiteSpace(localizationCulture)) - { - var lang = localizationCulture.Split('-')[0]; - - html = html.Replace("<html", "<html data-culture=\"" + localizationCulture + "\" lang=\"" + lang + "\"", StringComparison.Ordinal); - } - - if (isMainIndexPage) - { - html = html.Replace("<head>", "<head>" + GetMetaTags(mode), StringComparison.Ordinal); - } - - // Disable embedded scripts from plugins. We'll run them later once resources have loaded - if (html.IndexOf("<script", StringComparison.OrdinalIgnoreCase) != -1) - { - html = html.Replace("<script", "<!--<script", StringComparison.Ordinal); - html = html.Replace("</script>", "</script>-->", StringComparison.Ordinal); - } - - if (isMainIndexPage) - { - html = html.Replace("</body>", GetCommonJavascript(mode, appVersion) + "</body>", StringComparison.Ordinal); - } - - var bytes = Encoding.UTF8.GetBytes(html); - - return new MemoryStream(bytes); - } - - /// <summary> - /// Gets the meta tags. - /// </summary> - /// <returns>System.String.</returns> - private static string GetMetaTags(string mode) - { - var sb = new StringBuilder(); - - if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase) - || string.Equals(mode, "android", StringComparison.OrdinalIgnoreCase)) - { - sb.Append("<meta http-equiv=\"Content-Security-Policy\" content=\"default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: gap: file: filesystem: ws: wss:;\">"); - } - - return sb.ToString(); - } - - /// <summary> - /// Gets the common javascript. - /// </summary> - /// <param name="mode">The mode.</param> - /// <param name="version">The version.</param> - /// <returns>System.String.</returns> - private static string GetCommonJavascript(string mode, string version) - { - var builder = new StringBuilder(); - - builder.Append("<script>"); - if (!string.IsNullOrWhiteSpace(mode)) - { - builder.AppendFormat(CultureInfo.InvariantCulture, "window.appMode='{0}';", mode); - } - else - { - builder.AppendFormat(CultureInfo.InvariantCulture, "window.dashboardVersion='{0}';", version); - } - - builder.Append("</script>"); - - if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase)) - { - builder.Append("<script src=\"cordova.js\" defer></script>"); - } - - builder.Append("<script src=\"scripts/apploader.js"); - if (!string.IsNullOrWhiteSpace(version)) - { - builder.Append("?v="); - builder.Append(version); - } - - builder.Append("\" defer></script>"); - - return builder.ToString(); - } - } -} diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index da52b852a4..bcaee50f29 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{5624B7B5-B5A7-41D8-9F10-CC5611109619}</ProjectGuid> + </PropertyGroup> + <ItemGroup> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index e262820958..45fd9add92 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{23499896-B135-4527-8574-C26E926EA99E}</ProjectGuid> + </PropertyGroup> + <ItemGroup> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 36b9a9c1f8..5c8de80f17 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -208,8 +208,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected void ParseProviderLinks(T item, string xml) { - // Look for a match for the Regex pattern "tt" followed by 7 digits - var m = Regex.Match(xml, @"tt([0-9]{7})", RegexOptions.IgnoreCase); + // Look for a match for the Regex pattern "tt" followed by 7 or 8 digits + var m = Regex.Match(xml, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); if (m.Success) { item.SetProviderId(MetadataProviders.Imdb, m.Value); diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 1c84622ac0..e100c0b1cd 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,7 +1,9 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.3 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}" @@ -36,31 +38,39 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41093F42-C7CC-4D07-956B-6182CBEDE2EC}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + jellyfin.ruleset = jellyfin.ruleset SharedVersion.cs = SharedVersion.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api", "Jellyfin.Api\Jellyfin.Api.csproj", "{DFBEFB4C-DA19-4143-98B7-27320C7F7163}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api", "Jellyfin.Api\Jellyfin.Api.csproj", "{DFBEFB4C-DA19-4143-98B7-27320C7F7163}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}" + ProjectSection(SolutionItems) = preProject + tests\jellyfin-tests.ruleset = tests\jellyfin-tests.ruleset + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.MediaEncoding.Tests", "tests\Jellyfin.MediaEncoding.Tests\Jellyfin.MediaEncoding.Tests.csproj", "{28464062-0939-4AA7-9F7B-24DDDA61A7C0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Naming.Tests", "tests\Jellyfin.Naming.Tests\Jellyfin.Naming.Tests.csproj", "{3998657B-1CCC-49DD-A19F-275DC8495F57}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Tests", "tests\Jellyfin.MediaEncoding.Tests\Jellyfin.MediaEncoding.Tests.csproj", "{28464062-0939-4AA7-9F7B-24DDDA61A7C0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Naming.Tests", "tests\Jellyfin.Naming.Tests\Jellyfin.Naming.Tests.csproj", "{3998657B-1CCC-49DD-A19F-275DC8495F57}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -112,10 +122,6 @@ Global {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|Any CPU.Build.0 = Release|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -176,6 +182,18 @@ Global {462584F7-5023-4019-9EAC-B98CA458C0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {462584F7-5023-4019-9EAC-B98CA458C0A0}.Release|Any CPU.Build.0 = Release|Any CPU + {F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.Build.0 = Release|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU + {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -208,5 +226,6 @@ Global {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {7C93C84F-105C-48E5-A878-406FA0A5B296} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 681076e3e7..99a66e306a 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,99 @@ Most of the translations can be found in the web client but we have several othe <a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget"> <img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-web/multi-auto.svg" alt="Detailed Translation Status"/> </a> + +## 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. + +### Prerequisites + +Before the project can be built, you must first install the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) 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 Core development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download). + +### 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 +``` + +### 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=11). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=11&_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` + +Once you have a copy of the built web client files, you need to copy them into a specific directory. + +> `<repository root>/Mediabrowser.WebDashboard/jellyfin-web` + +As part of the build process, this folder will be copied to the build output directory, where it can be accessed by the server. + +### 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 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 # 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 bin/Debug/netcoreapp3.1 # 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 `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` 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. diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index 9753ae9b1f..e3f3127b64 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{21002819-C39A-4D3E-BE83-2A276A77FB1F}</ProjectGuid> + </PropertyGroup> + <ItemGroup> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> diff --git a/SharedVersion.cs b/SharedVersion.cs index d741f379d2..6981c1ca9d 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.5.0")] -[assembly: AssemblyFileVersion("10.5.0")] +[assembly: AssemblyVersion("10.6.0")] +[assembly: AssemblyFileVersion("10.6.0")] diff --git a/build b/build deleted file mode 100755 index 95d5d5c495..0000000000 --- a/build +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env bash - -# build - build Jellyfin binaries or packages - -set -o errexit -set -o pipefail - -# The list of possible package actions (except 'clean') -declare -a actions=( 'build' 'package' 'sign' 'publish' ) - -# The list of possible platforms, based on directories under 'deployment/' -declare -a platforms=( $( - find deployment/ -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | sort -) ) - -# The list of standard dependencies required by all build scripts; individual -# action scripts may specify their own dependencies -declare -a dependencies=( 'tar' 'zip' ) - -usage() { - echo -e "build - build Jellyfin binaries or packages" - echo -e "" - echo -e "Usage:" - echo -e " $ build --list-platforms" - echo -e " $ build --list-actions <platform>" - echo -e " $ build [-k/--keep-artifacts] [-b/--web-branch <web_branch>] <platform> <action>" - echo -e "" - echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds." - echo -e "The web_branch defaults to the same branch name as the current main branch or can be 'local' to not touch the submodule branching." - echo -e "To build all platforms, use 'all'." - echo -e "To perform all build actions, use 'all'." - echo -e "Build output files are collected at '../bin/<platform>'." -} - -# Show usage on stderr with exit 1 on argless -if [[ -z $1 ]]; then - usage >&2 - exit 1 -fi -# Show usage if -h or --help are specified in the args -if [[ $@ =~ '-h' || $@ =~ '--help' ]]; then - usage - exit 0 -fi - -# List all available platforms then exit -if [[ $1 == '--list-platforms' ]]; then - echo -e "Available platforms:" - for platform in ${platforms[@]}; do - echo -e " ${platform}" - done - exit 0 -fi - -# List all available actions for a given platform then exit -if [[ $1 == '--list-actions' ]]; then - platform="$2" - if [[ ! " ${platforms[@]} " =~ " ${platform} " ]]; then - echo "ERROR: Platform ${platform} does not exist." - exit 1 - fi - echo -e "Available actions for platform ${platform}:" - for action in ${actions[@]}; do - if [[ -f deployment/${platform}/${action}.sh ]]; then - echo -e " ${action}" - fi - done - exit 0 -fi - -# Parse keep-artifacts option -if [[ $1 == '-k' || $1 == '--keep-artifacts' ]]; then - keep_artifacts="y" - shift 1 -else - keep_artifacts="n" -fi - -# Parse branch option -if [[ $1 == '-b' || $1 == '--web-branch' ]]; then - web_branch="$2" - shift 2 -else - web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )" -fi - -# Parse platform option -if [[ -n $1 ]]; then - cli_platform="$1" - shift -else - echo "ERROR: A platform must be specified. Use 'all' to specify all platforms." - exit 1 -fi -if [[ ${cli_platform} == 'all' ]]; then - declare -a platform=( ${platforms[@]} ) -else - if [[ ! " ${platforms[@]} " =~ " ${cli_platform} " ]]; then - echo "ERROR: Platform ${cli_platform} is invalid. Use the '--list-platforms' option to list available platforms." - exit 1 - else - declare -a platform=( "${cli_platform}" ) - fi -fi - -# Parse action option -if [[ -n $1 ]]; then - cli_action="$1" - shift -else - echo "ERROR: An action must be specified. Use 'all' to specify all actions." - exit 1 -fi -if [[ ${cli_action} == 'all' ]]; then - declare -a action=( ${actions[@]} ) -else - if [[ ! " ${actions[@]} " =~ " ${cli_action} " ]]; then - echo "ERROR: Action ${cli_action} is invalid. Use the '--list-actions <platform>' option to list available actions." - exit 1 - else - declare -a action=( "${cli_action}" ) - fi -fi - -# Verify required utilities are installed -missing_deps=() -for utility in ${dependencies[@]}; do - if ! which ${utility} &>/dev/null; then - missing_deps+=( ${utility} ) - fi -done - -# Error if we're missing anything -if [[ ${#missing_deps[@]} -gt 0 ]]; then - echo -e "ERROR: This script requires the following missing utilities:" - for utility in ${missing_deps[@]}; do - echo -e " ${utility}" - done - exit 1 -fi - -# Parse platform-specific dependencies -for target_platform in ${platform[@]}; do - # Read platform-specific dependencies - if [[ -f deployment/${target_platform}/dependencies.txt ]]; then - platform_dependencies="$( grep -v '^#' deployment/${target_platform}/dependencies.txt )" - - # Verify required utilities are installed - missing_deps=() - for utility in ${platform_dependencies[@]}; do - if ! which ${utility} &>/dev/null; then - missing_deps+=( ${utility} ) - fi - done - - # Error if we're missing anything - if [[ ${#missing_deps[@]} -gt 0 ]]; then - echo -e "ERROR: The ${target_platform} platform requires the following utilities:" - for utility in ${missing_deps[@]}; do - echo -e " ${utility}" - done - exit 1 - fi - fi -done - -# Execute each platform and action in order, if said action is enabled -pushd deployment/ -for target_platform in ${platform[@]}; do - echo -e "> Processing platform ${target_platform}" - date_start=$( date +%s ) - pushd ${target_platform} - cleanup() { - echo -e ">> Processing action clean" - if [[ -f clean.sh && -x clean.sh ]]; then - ./clean.sh ${keep_artifacts} - fi - } - trap cleanup EXIT INT - for target_action in ${action[@]}; do - echo -e ">> Processing action ${target_action}" - if [[ -f ${target_action}.sh && -x ${target_action}.sh ]]; then - ./${target_action}.sh web_branch=${web_branch} - fi - done - if [[ -d pkg-dist/ ]]; then - echo -e ">> Collecting build artifacts" - target_dir="../../../bin/${target_platform}" - mkdir -p ${target_dir} - mv pkg-dist/* ${target_dir}/ - fi - cleanup - date_end=$( date +%s ) - echo -e "> Completed platform ${target_platform} in $( expr ${date_end} - ${date_start} ) seconds." - popd -done -popd diff --git a/build b/build new file mode 120000 index 0000000000..c07a74de4f --- /dev/null +++ b/build @@ -0,0 +1 @@ +build.sh \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000000..1db02af983 --- /dev/null +++ b/build.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +# build.sh - Build Jellyfin binary packages +# Part of the Jellyfin Project + +set -o errexit +set -o pipefail + +usage() { + echo -e "build.sh - Build Jellyfin binary packages" + echo -e "Usage:" + echo -e " $0 -t/--type <BUILD_TYPE> -p/--platform <PLATFORM> [-k/--keep-artifacts] [-l/--list-platforms]" + echo -e "Notes:" + echo -e " * BUILD_TYPE can be one of: [native, docker] and must be specified" + echo -e " * native: Build using the build script in the host OS" + echo -e " * docker: Build using the build script in a standardized Docker container" + echo -e " * PLATFORM can be any platform shown by -l/--list-platforms and must be specified" + echo -e " * If -k/--keep-artifacts is specified, transient artifacts (e.g. Docker containers) will be" + echo -e " retained after the build is finished; the source directory will still be cleaned" + echo -e " * If -l/--list-platforms is specified, all other arguments are ignored; the script will print" + echo -e " the list of supported platforms and exit" +} + +list_platforms() { + declare -a platforms + platforms=( + $( find deployment -maxdepth 1 -mindepth 1 -name "build.*" | awk -F'.' '{ $1=""; printf $2; if ($3 != ""){ printf "." $3; }; if ($4 != ""){ printf "." $4; }; print ""; }' | sort ) + ) + echo -e "Valid platforms:" + echo + for platform in ${platforms[@]}; do + echo -e "* ${platform} : $( grep '^#=' deployment/build.${platform} | sed 's/^#= //' )" + done +} + +do_build_native() { + if [[ ! -f $( which dpkg ) || $( dpkg --print-architecture | head -1 ) != "${PLATFORM##*.}" ]]; then + echo "Cross-building is not supported for native builds, use 'docker' builds on amd64 for cross-building." + exit 1 + fi + export IS_DOCKER=NO + deployment/build.${PLATFORM} +} + +do_build_docker() { + if [[ -f $( which dpkg ) && $( dpkg --print-architecture | head -1 ) != "amd64" ]]; then + echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead." + exit 1 + fi + if [[ ! -f deployment/Dockerfile.${PLATFORM} ]]; then + echo "Missing Dockerfile for platform ${PLATFORM}" + exit 1 + fi + if [[ ${KEEP_ARTIFACTS} == YES ]]; then + docker_args="" + else + docker_args="--rm" + fi + + docker build . -t "jellyfin-builder.${PLATFORM}" -f deployment/Dockerfile.${PLATFORM} + mkdir -p ${ARTIFACT_DIR} + docker run $docker_args -v "${SOURCE_DIR}:/jellyfin" -v "${ARTIFACT_DIR}:/dist" "jellyfin-builder.${PLATFORM}" +} + +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + -t|--type) + BUILD_TYPE="$2" + shift # past argument + shift # past value + ;; + -p|--platform) + PLATFORM="$2" + shift # past argument + shift # past value + ;; + -k|--keep-artifacts) + KEEP_ARTIFACTS=YES + shift # past argument + ;; + -l|--list-platforms) + list_platforms + exit 0 + ;; + -h|--help) + usage + exit 0 + ;; + *) # unknown option + echo "Unknown option $1" + usage + exit 1 + ;; + esac +done + +if [[ -z ${BUILD_TYPE} || -z ${PLATFORM} ]]; then + usage + exit 1 +fi + +export SOURCE_DIR="$( pwd )" +export ARTIFACT_DIR="${SOURCE_DIR}/../bin/${PLATFORM}" + +# Determine build type +case ${BUILD_TYPE} in + native) + do_build_native + ;; + docker) + do_build_docker + ;; +esac diff --git a/build.yaml b/build.yaml index 123f77fb89..9e590e5a01 100644 --- a/build.yaml +++ b/build.yaml @@ -1,18 +1,17 @@ --- # We just wrap `build` so this is really it name: "jellyfin" -version: "10.5.0" +version: "10.6.0" packages: - - debian-package-x64 - - debian-package-armhf - - debian-package-arm64 - - ubuntu-package-x64 - - ubuntu-package-armhf - - ubuntu-package-arm64 - - fedora-package-x64 - - centos-package-x64 - - linux-x64 + - debian.amd64 + - debian.arm64 + - debian.armhf + - ubuntu.amd64 + - ubuntu.arm64 + - ubuntu.armhf + - fedora.amd64 + - centos.amd64 + - linux.amd64 + - windows.amd64 - macos - portable - - win-x64 - - win-x86 diff --git a/bump_version b/bump_version index 106dd7a78e..46b7f86e05 100755 --- a/bump_version +++ b/bump_version @@ -10,10 +10,6 @@ usage() { echo -e "" echo -e "Usage:" echo -e " $ bump_version <new_version>" - echo -e "" - echo -e "The web_branch defaults to the same branch name as the current main branch." - echo -e "This helps facilitate releases where both branches would be called release-X.Y.Z" - echo -e "and would already be created before running this script." } if [[ -z $1 ]]; then @@ -54,37 +50,26 @@ else new_version_deb="${new_version}-1" fi -# Set the Dockerfile web version to the specified new_version -old_version="$( - grep "JELLYFIN_WEB_VERSION=" Dockerfile \ - | sed -E 's/ARG JELLYFIN_WEB_VERSION=v([0-9\.]+[-a-z0-9]*)/\1/' -)" -echo $old_version - -old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars -sed -i "s/${old_version_sed}/${new_version}/g" Dockerfile* +# Update the metapackage equivs file +debian_equivs_file="debian/metapackage/jellyfin" +sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file} # Write out a temporary Debian changelog with our new stuff appended and some templated formatting -debian_changelog_file="deployment/debian-package-x64/pkg-src/changelog" +debian_changelog_file="debian/changelog" debian_changelog_temp="$( mktemp )" # Create new temp file with our changelog -echo -e "### DEBIAN PACKAGE CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit. -jellyfin (${new_version_deb}) unstable; urgency=medium +echo -e "jellyfin (${new_version_deb}) unstable; urgency=medium * New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version} -- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 ) " >> ${debian_changelog_temp} cat ${debian_changelog_file} >> ${debian_changelog_temp} -# Edit the file to verify -$EDITOR ${debian_changelog_temp} # Move into place mv ${debian_changelog_temp} ${debian_changelog_file} -# Clean up -rm -f ${debian_changelog_temp} # Write out a temporary Yum changelog with our new stuff prepended and some templated formatting -fedora_spec_file="deployment/fedora-package-x64/pkg-src/jellyfin.spec" +fedora_spec_file="fedora/jellyfin.spec" fedora_changelog_temp="$( mktemp )" fedora_spec_temp_dir="$( mktemp -d )" fedora_spec_temp="${fedora_spec_temp_dir}/jellyfin.spec.tmp" @@ -98,21 +83,18 @@ sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00 # Remove the header from xx01 sed -i '/^%changelog/d' xx01 # Create new temp file with our changelog -echo -e "### YUM SPEC CHANGELOG: Verify this file looks correct or edit accordingly, then delete this line, write, and exit. -%changelog +echo -e "%changelog * $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org> - New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}" >> ${fedora_changelog_temp} cat xx01 >> ${fedora_changelog_temp} -# Edit the file to verify -$EDITOR ${fedora_changelog_temp} # Reassembble cat xx00 ${fedora_changelog_temp} > ${fedora_spec_temp} popd # Move into place mv ${fedora_spec_temp} ${fedora_spec_file} # Clean up -rm -rf ${fedora_changelog_temp} ${fedora_spec_temp_dir} +rm -rf ${fedora_spec_temp_dir} # Stage the changed files for commit -git add ${shared_version_file} ${build_file} ${debian_changelog_file} ${fedora_spec_file} Dockerfile* +git add ${shared_version_file} ${build_file} ${debian_equivs_file} ${debian_changelog_file} ${fedora_spec_file} git status diff --git a/deployment/debian-package-x64/pkg-src/bin/restart.sh b/debian/bin/restart.sh similarity index 100% rename from deployment/debian-package-x64/pkg-src/bin/restart.sh rename to debian/bin/restart.sh diff --git a/deployment/debian-package-x64/pkg-src/changelog b/debian/changelog similarity index 93% rename from deployment/debian-package-x64/pkg-src/changelog rename to debian/changelog index 51c4822370..35fb659571 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +jellyfin-server (10.6.0-1) unstable; urgency=medium + + * Forthcoming stable release + + -- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 23 Mar 2020 14:46:05 -0400 + jellyfin (10.5.0-1) unstable; urgency=medium * New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0 diff --git a/deployment/debian-package-x64/pkg-src/compat b/debian/compat similarity index 100% rename from deployment/debian-package-x64/pkg-src/compat rename to debian/compat diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin b/debian/conf/jellyfin similarity index 81% rename from deployment/debian-package-x64/pkg-src/conf/jellyfin rename to debian/conf/jellyfin index c6e595f15a..64c98520cb 100644 --- a/deployment/debian-package-x64/pkg-src/conf/jellyfin +++ b/debian/conf/jellyfin @@ -18,6 +18,9 @@ JELLYFIN_CONFIG_DIR="/etc/jellyfin" JELLYFIN_LOG_DIR="/var/log/jellyfin" JELLYFIN_CACHE_DIR="/var/cache/jellyfin" +# web client path, installed by the jellyfin-web package +JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web" + # Restart script for in-app server control JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh" @@ -37,4 +40,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg" # Application username JELLYFIN_USER="jellyfin" # Full application command -JELLYFIN_ARGS="$JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT" +JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT" diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin-sudoers b/debian/conf/jellyfin-sudoers similarity index 100% rename from deployment/debian-package-x64/pkg-src/conf/jellyfin-sudoers rename to debian/conf/jellyfin-sudoers diff --git a/deployment/debian-package-x64/pkg-src/conf/jellyfin.service.conf b/debian/conf/jellyfin.service.conf similarity index 100% rename from deployment/debian-package-x64/pkg-src/conf/jellyfin.service.conf rename to debian/conf/jellyfin.service.conf diff --git a/deployment/debian-package-x64/pkg-src/conf/logging.json b/debian/conf/logging.json similarity index 100% rename from deployment/debian-package-x64/pkg-src/conf/logging.json rename to debian/conf/logging.json diff --git a/deployment/debian-package-x64/pkg-src/control b/debian/control similarity index 59% rename from deployment/debian-package-x64/pkg-src/control rename to debian/control index 13fd3ecabb..896d8286b7 100644 --- a/deployment/debian-package-x64/pkg-src/control +++ b/debian/control @@ -1,4 +1,4 @@ -Source: jellyfin +Source: jellyfin-server Section: misc Priority: optional Maintainer: Jellyfin Team <team@jellyfin.org> @@ -8,24 +8,23 @@ Build-Depends: debhelper (>= 9), libcurl4-openssl-dev, libfontconfig1-dev, libfreetype6-dev, - libssl-dev, - wget, - npm | nodejs + libssl-dev Standards-Version: 3.9.4 -Homepage: https://jellyfin.media/ +Homepage: https://jellyfin.org/ Vcs-Git: https://github.org/jellyfin/jellyfin.git Vcs-Browser: https://github.org/jellyfin/jellyfin -Package: jellyfin +Package: jellyfin-server Replaces: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server Breaks: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server Architecture: any Depends: at, libsqlite3-0, - jellyfin-ffmpeg, + jellyfin-ffmpeg (>= 4.2.1-2), libfontconfig1, libfreetype6, libssl1.1 -Description: Jellyfin is a home media server. - It is built on top of other popular open source technologies such as Service Stack, jQuery, jQuery mobile, and Mono. It features a REST-based api with built-in documentation to facilitate client development. We also have client libraries for our api to enable rapid development. +Recommends: jellyfin-web +Description: Jellyfin is the Free Software Media System. + This package provides the Jellyfin server backend and API. diff --git a/deployment/debian-package-x64/pkg-src/copyright b/debian/copyright similarity index 100% rename from deployment/debian-package-x64/pkg-src/copyright rename to debian/copyright diff --git a/deployment/debian-package-x64/pkg-src/gbp.conf b/debian/gbp.conf similarity index 100% rename from deployment/debian-package-x64/pkg-src/gbp.conf rename to debian/gbp.conf diff --git a/deployment/debian-package-x64/pkg-src/install b/debian/install similarity index 100% rename from deployment/debian-package-x64/pkg-src/install rename to debian/install diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.init b/debian/jellyfin.init similarity index 100% rename from deployment/debian-package-x64/pkg-src/jellyfin.init rename to debian/jellyfin.init diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.service b/debian/jellyfin.service similarity index 60% rename from deployment/debian-package-x64/pkg-src/jellyfin.service rename to debian/jellyfin.service index 1305e238b0..f1a8f4652c 100644 --- a/deployment/debian-package-x64/pkg-src/jellyfin.service +++ b/debian/jellyfin.service @@ -6,7 +6,7 @@ After = network.target Type = simple EnvironmentFile = /etc/default/jellyfin User = jellyfin -ExecStart = /usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} +ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} Restart = on-failure TimeoutSec = 15 diff --git a/deployment/debian-package-x64/pkg-src/jellyfin.upstart b/debian/jellyfin.upstart similarity index 100% rename from deployment/debian-package-x64/pkg-src/jellyfin.upstart rename to debian/jellyfin.upstart diff --git a/debian/metapackage/jellyfin b/debian/metapackage/jellyfin new file mode 100644 index 0000000000..9a41eafaed --- /dev/null +++ b/debian/metapackage/jellyfin @@ -0,0 +1,13 @@ +Source: jellyfin +Section: misc +Priority: optional +Homepage: https://jellyfin.org +Standards-Version: 3.9.2 + +Package: jellyfin +Version: 10.6.0 +Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org> +Depends: jellyfin-server, jellyfin-web +Description: Provides the Jellyfin Free Software Media System + Provides the full Jellyfin experience, including both the server and web interface. + diff --git a/deployment/debian-package-x64/pkg-src/po/POTFILES.in b/debian/po/POTFILES.in similarity index 100% rename from deployment/debian-package-x64/pkg-src/po/POTFILES.in rename to debian/po/POTFILES.in diff --git a/deployment/debian-package-x64/pkg-src/po/templates.pot b/debian/po/templates.pot similarity index 100% rename from deployment/debian-package-x64/pkg-src/po/templates.pot rename to debian/po/templates.pot diff --git a/deployment/debian-package-x64/pkg-src/postinst b/debian/postinst similarity index 100% rename from deployment/debian-package-x64/pkg-src/postinst rename to debian/postinst diff --git a/deployment/debian-package-x64/pkg-src/postrm b/debian/postrm similarity index 100% rename from deployment/debian-package-x64/pkg-src/postrm rename to debian/postrm diff --git a/deployment/debian-package-x64/pkg-src/preinst b/debian/preinst similarity index 100% rename from deployment/debian-package-x64/pkg-src/preinst rename to debian/preinst diff --git a/deployment/debian-package-x64/pkg-src/prerm b/debian/prerm similarity index 100% rename from deployment/debian-package-x64/pkg-src/prerm rename to debian/prerm diff --git a/deployment/debian-package-x64/pkg-src/rules b/debian/rules similarity index 64% rename from deployment/debian-package-x64/pkg-src/rules rename to debian/rules index c2d57dfb22..2a5d41a696 100755 --- a/deployment/debian-package-x64/pkg-src/rules +++ b/debian/rules @@ -2,8 +2,6 @@ CONFIG := Release TERM := xterm SHELL := /bin/bash -WEB_TARGET := $(CURDIR)/MediaBrowser.WebDashboard/jellyfin-web -WEB_VERSION := $(shell sed -n -e 's/^version: "\(.*\)"/\1/p' $(CURDIR)/build.yaml) HOST_ARCH := $(shell arch) BUILD_ARCH := ${DEB_HOST_MULTIARCH} @@ -41,25 +39,12 @@ override_dh_auto_test: override_dh_clistrip: override_dh_auto_build: - echo $(WEB_VERSION) - # Clone down and build Web frontend - mkdir -p $(WEB_TARGET) - wget -O web-src.tgz https://github.com/jellyfin/jellyfin-web/archive/v$(WEB_VERSION).tar.gz || wget -O web-src.tgz https://github.com/jellyfin/jellyfin-web/archive/master.tar.gz - mkdir -p $(CURDIR)/web - tar -xzf web-src.tgz -C $(CURDIR)/web/ --strip 1 - cd $(CURDIR)/web/ && npm install yarn - cd $(CURDIR)/web/ && node_modules/yarn/bin/yarn install - mv $(CURDIR)/web/dist/* $(WEB_TARGET)/ - # Build the application dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server override_dh_auto_clean: dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true - rm -f '$(CURDIR)/web-src.tgz' rm -rf '$(CURDIR)/usr' - rm -rf '$(CURDIR)/web' - rm -rf '$(WEB_TARGET)' # Force the service name to jellyfin even if we're building jellyfin-nightly override_dh_installinit: diff --git a/deployment/debian-package-x64/pkg-src/source.lintian-overrides b/debian/source.lintian-overrides similarity index 100% rename from deployment/debian-package-x64/pkg-src/source.lintian-overrides rename to debian/source.lintian-overrides diff --git a/deployment/debian-package-x64/pkg-src/source/format b/debian/source/format similarity index 100% rename from deployment/debian-package-x64/pkg-src/source/format rename to debian/source/format diff --git a/deployment/debian-package-x64/pkg-src/source/options b/debian/source/options similarity index 100% rename from deployment/debian-package-x64/pkg-src/source/options rename to debian/source/options diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 new file mode 100644 index 0000000000..39788cc0ee --- /dev/null +++ b/deployment/Dockerfile.centos.amd64 @@ -0,0 +1,32 @@ +FROM centos:7 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=3.1 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV IS_DOCKER=YES + +# Prepare CentOS environment +RUN yum update -y \ + && yum install -y epel-release \ + && yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git + +# Install DotNET SDK +RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \ + && rpmdev-setuptree \ + && yum install -y dotnet-sdk-${SDK_VERSION} + +# Create symlinks and directories +RUN ln -sf ${SOURCE_DIR}/deployment/build.centos.amd64 /build.sh \ + && mkdir -p ${SOURCE_DIR}/SPECS \ + && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ + && mkdir -p ${SOURCE_DIR}/SOURCES \ + && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES + +VOLUME ${SOURCE_DIR}/ + +VOLUME ${ARTIFACT_DIR}/ + +ENTRYPOINT ["/build.sh"] diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64 new file mode 100644 index 0000000000..b5a0380489 --- /dev/null +++ b/deployment/Dockerfile.debian.amd64 @@ -0,0 +1,31 @@ +FROM debian:10 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=3.1 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 +ENV IS_DOCKER=YES + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Link to build script +RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.amd64 /build.sh + +VOLUME ${SOURCE_DIR}/ + +VOLUME ${ARTIFACT_DIR}/ + +ENTRYPOINT ["/build.sh"] diff --git a/deployment/debian-package-arm64/Dockerfile.amd64 b/deployment/Dockerfile.debian.arm64 similarity index 78% rename from deployment/debian-package-arm64/Dockerfile.amd64 rename to deployment/Dockerfile.debian.arm64 index b63e08b7dd..cfe562df33 100644 --- a/deployment/debian-package-arm64/Dockerfile.amd64 +++ b/deployment/Dockerfile.debian.arm64 @@ -1,7 +1,6 @@ FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64 ARG ARTIFACT_DIR=/dist ARG SDK_VERSION=3.1 # Docker run environment @@ -9,10 +8,11 @@ ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs ENV ARCH=amd64 +ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current @@ -29,15 +29,11 @@ RUN dpkg --add-architecture arm64 \ && cd cross-gcc-packages-amd64/cross-gcc-8-arm64 \ && apt-get install -y gcc-8-source libstdc++-8-dev-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust0:arm64 libstdc++-8-dev:arm64 -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh +# Link to build script +RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.arm64 /build.sh -# Link to Debian source dir; mkdir needed or it fails, can't force dest -RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian +VOLUME ${SOURCE_DIR}/ VOLUME ${ARTIFACT_DIR}/ -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] -#ENTRYPOINT ["/bin/bash"] +ENTRYPOINT ["/build.sh"] diff --git a/deployment/debian-package-armhf/Dockerfile.amd64 b/deployment/Dockerfile.debian.armhf similarity index 79% rename from deployment/debian-package-armhf/Dockerfile.amd64 rename to deployment/Dockerfile.debian.armhf index 1b64b53148..ea8c8c8e62 100644 --- a/deployment/debian-package-armhf/Dockerfile.amd64 +++ b/deployment/Dockerfile.debian.armhf @@ -1,7 +1,6 @@ FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf ARG ARTIFACT_DIR=/dist ARG SDK_VERSION=3.1 # Docker run environment @@ -9,10 +8,11 @@ ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs ENV ARCH=amd64 +ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current @@ -29,14 +29,11 @@ RUN dpkg --add-architecture armhf \ && cd cross-gcc-packages-amd64/cross-gcc-8-armhf \ && apt-get install -y gcc-8-source libstdc++-8-dev-armhf-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip binutils-arm-linux-gnueabihf libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf libssl-dev:armhf liblttng-ust0:armhf libstdc++-8-dev:armhf -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh +# Link to build script +RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh -# Link to Debian source dir; mkdir needed or it fails, can't force dest -RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian +VOLUME ${SOURCE_DIR}/ VOLUME ${ARTIFACT_DIR}/ -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] +ENTRYPOINT ["/build.sh"] diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/Dockerfile.fedora.amd64 similarity index 60% rename from deployment/fedora-package-x64/Dockerfile rename to deployment/Dockerfile.fedora.amd64 index 87120f3a05..01b99deb6d 100644 --- a/deployment/fedora-package-x64/Dockerfile +++ b/deployment/Dockerfile.fedora.amd64 @@ -1,18 +1,16 @@ FROM fedora:31 # Docker build arguments ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/fedora-package-x64 ARG ARTIFACT_DIR=/dist ARG SDK_VERSION=3.1 # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist +ENV IS_DOCKER=YES # Prepare Fedora environment -RUN dnf update -y - -# Install build dependencies -RUN dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs-yarn +RUN dnf update -y \ + && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel # Install DotNET SDK RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc \ @@ -20,14 +18,14 @@ RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc \ && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} # Create symlinks and directories -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ +RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \ && mkdir -p ${SOURCE_DIR}/SPECS \ - && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ + && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ && mkdir -p ${SOURCE_DIR}/SOURCES \ - && ln -s ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/SOURCES + && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES -VOLUME ${ARTIFACT_DIR}/ +VOLUME ${SOURCE_DIR}/ -COPY . ${SOURCE_DIR}/ +VOLUME ${ARTIFACT_DIR}/ -ENTRYPOINT ["/docker-build.sh"] +ENTRYPOINT ["/build.sh"] diff --git a/deployment/macos/Dockerfile b/deployment/Dockerfile.linux.amd64 similarity index 70% rename from deployment/macos/Dockerfile rename to deployment/Dockerfile.linux.amd64 index b522df8848..d8bec92145 100644 --- a/deployment/macos/Dockerfile +++ b/deployment/Dockerfile.linux.amd64 @@ -1,7 +1,6 @@ FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/macos ARG ARTIFACT_DIR=/dist ARG SDK_VERSION=3.1 # Docker run environment @@ -9,6 +8,7 @@ ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs ENV ARCH=amd64 +ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update \ @@ -21,17 +21,11 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4 && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet -# Install yarn package manager -RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ - && apt update \ - && apt install -y yarn - # Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh +RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.amd64 /build.sh -VOLUME ${ARTIFACT_DIR}/ +VOLUME ${SOURCE_DIR}/ -COPY . ${SOURCE_DIR}/ +VOLUME ${ARTIFACT_DIR}/ -ENTRYPOINT ["/docker-build.sh"] +ENTRYPOINT ["/build.sh"] diff --git a/deployment/portable/Dockerfile b/deployment/Dockerfile.macos similarity index 70% rename from deployment/portable/Dockerfile rename to deployment/Dockerfile.macos index 965eb82b86..ba5da40190 100644 --- a/deployment/portable/Dockerfile +++ b/deployment/Dockerfile.macos @@ -1,7 +1,6 @@ FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/portable ARG ARTIFACT_DIR=/dist ARG SDK_VERSION=3.1 # Docker run environment @@ -9,6 +8,7 @@ ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs ENV ARCH=amd64 +ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update \ @@ -21,17 +21,11 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4 && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet -# Install yarn package manager -RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ - && apt update \ - && apt install -y yarn - # Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh +RUN ln -sf ${SOURCE_DIR}/deployment/build.macos /build.sh -VOLUME ${ARTIFACT_DIR}/ +VOLUME ${SOURCE_DIR}/ -COPY . ${SOURCE_DIR}/ +VOLUME ${ARTIFACT_DIR}/ -ENTRYPOINT ["/docker-build.sh"] +ENTRYPOINT ["/build.sh"] diff --git a/deployment/linux-x64/Dockerfile b/deployment/Dockerfile.portable similarity index 69% rename from deployment/linux-x64/Dockerfile rename to deployment/Dockerfile.portable index c47057546d..2893e140df 100644 --- a/deployment/linux-x64/Dockerfile +++ b/deployment/Dockerfile.portable @@ -1,14 +1,13 @@ FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/linux-x64 ARG ARTIFACT_DIR=/dist ARG SDK_VERSION=3.1 # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs -ENV ARCH=amd64 +ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update \ @@ -21,17 +20,11 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4 && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet -# Install yarn package manager -RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ - && apt update \ - && apt install -y yarn - # Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh +RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh -VOLUME ${ARTIFACT_DIR}/ +VOLUME ${SOURCE_DIR}/ -COPY . ${SOURCE_DIR}/ +VOLUME ${ARTIFACT_DIR}/ -ENTRYPOINT ["/docker-build.sh"] +ENTRYPOINT ["/build.sh"] diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 new file mode 100644 index 0000000000..e61be4efcc --- /dev/null +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -0,0 +1,31 @@ +FROM ubuntu:bionic +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=3.1 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 +ENV IS_DOCKER=YES + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Link to build script +RUN ln -sf ${SOURCE_DIR}/deployment/build.ubuntu.amd64 /build.sh + +VOLUME ${SOURCE_DIR}/ + +VOLUME ${ARTIFACT_DIR}/ + +ENTRYPOINT ["/build.sh"] diff --git a/deployment/ubuntu-package-arm64/Dockerfile.amd64 b/deployment/Dockerfile.ubuntu.arm64 similarity index 81% rename from deployment/ubuntu-package-arm64/Dockerfile.amd64 rename to deployment/Dockerfile.ubuntu.arm64 index b11994a18a..f91b91cd46 100644 --- a/deployment/ubuntu-package-arm64/Dockerfile.amd64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -1,7 +1,6 @@ FROM ubuntu:bionic # Docker build arguments ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64 ARG ARTIFACT_DIR=/dist ARG SDK_VERSION=3.1 # Docker run environment @@ -9,6 +8,7 @@ ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs ENV ARCH=amd64 +ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update \ @@ -21,12 +21,6 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4 && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet -# Install npm package manager -RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ - && apt update \ - && apt install -y nodejs - # Prepare the cross-toolchain RUN rm /etc/apt/sources.list \ && export CODENAME="$( lsb_release -c -s )" \ @@ -46,14 +40,11 @@ RUN rm /etc/apt/sources.list \ && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ && apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64 libssl-dev:arm64 -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh +# Link to build script +RUN ln -sf ${SOURCE_DIR}/deployment/build.ubuntu.arm64 /build.sh -# Link to Debian source dir; mkdir needed or it fails, can't force dest -RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian +VOLUME ${SOURCE_DIR}/ VOLUME ${ARTIFACT_DIR}/ -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] +ENTRYPOINT ["/build.sh"] diff --git a/deployment/ubuntu-package-armhf/Dockerfile.amd64 b/deployment/Dockerfile.ubuntu.armhf similarity index 81% rename from deployment/ubuntu-package-armhf/Dockerfile.amd64 rename to deployment/Dockerfile.ubuntu.armhf index e475b14389..85414614c0 100644 --- a/deployment/ubuntu-package-armhf/Dockerfile.amd64 +++ b/deployment/Dockerfile.ubuntu.armhf @@ -1,7 +1,6 @@ FROM ubuntu:bionic # Docker build arguments ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-armhf ARG ARTIFACT_DIR=/dist ARG SDK_VERSION=3.1 # Docker run environment @@ -9,6 +8,7 @@ ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs ENV ARCH=amd64 +ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update \ @@ -21,12 +21,6 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4 && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet -# Install npm package manager -RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ - && apt update \ - && apt install -y nodejs - # Prepare the cross-toolchain RUN rm /etc/apt/sources.list \ && export CODENAME="$( lsb_release -c -s )" \ @@ -46,14 +40,11 @@ RUN rm /etc/apt/sources.list \ && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf libssl-dev:armhf -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh +# Link to build script +RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh -# Link to Debian source dir; mkdir needed or it fails, can't force dest -RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian +VOLUME ${SOURCE_DIR}/ VOLUME ${ARTIFACT_DIR}/ -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] +ENTRYPOINT ["/build.sh"] diff --git a/deployment/win-x64/Dockerfile b/deployment/Dockerfile.windows.amd64 similarity index 69% rename from deployment/win-x64/Dockerfile rename to deployment/Dockerfile.windows.amd64 index 8a33749541..0397a023e2 100644 --- a/deployment/win-x64/Dockerfile +++ b/deployment/Dockerfile.windows.amd64 @@ -1,14 +1,13 @@ FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/win-x64 ARG ARTIFACT_DIR=/dist ARG SDK_VERSION=3.1 # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs -ENV ARCH=amd64 +ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update \ @@ -21,17 +20,11 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4 && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet -# Install yarn package manager -RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ - && apt update \ - && apt install -y yarn - # Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh +RUN ln -sf ${SOURCE_DIR}/deployment/build.windows.amd64 /build.sh -VOLUME ${ARTIFACT_DIR}/ +VOLUME ${SOURCE_DIR}/ -COPY . ${SOURCE_DIR}/ +VOLUME ${ARTIFACT_DIR}/ -ENTRYPOINT ["/docker-build.sh"] +ENTRYPOINT ["/build.sh"] diff --git a/deployment/README.md b/deployment/README.md deleted file mode 100644 index a805f59ca3..0000000000 --- a/deployment/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Jellyfin Packaging - -This directory contains the packaging configuration of Jellyfin for multiple platforms. The specification is below; all package platforms must follow the specification to be compatable with the central `build` script. - -## Package List - -### Operating System Packages - -* `debian-package-x64`: Package for Debian and Ubuntu amd64 systems. -* `fedora-package-x64`: Package for Fedora, CentOS, and Red Hat Enterprise Linux amd64 systems. - -### Portable Builds (archives) - -* `linux-x64`: Portable binary archive for generic Linux amd64 systems. -* `macos`: Portable binary archive for MacOS amd64 systems. -* `win-x64`: Portable binary archive for Windows amd64 systems. -* `win-x86`: Portable binary archive for Windows i386 systems. - -### Other Builds - -These builds are not necessarily run from the `build` script, but are present for other platforms. - -* `portable`: Compiled `.dll` for use with .NET Core runtime on any system. -* `docker`: Docker manifests for auto-publishing. -* `unraid`: unRaid Docker template; not built by `build` but imported into unRaid directly. -* `windows`: Support files and scripts for Windows CI build. - -## Package Specification - -### Dependencies - -* If a platform requires additional build dependencies, the required binary names, i.e. to validate `which <binary>`, should be specified in a `dependencies.txt` file inside the platform directory. - -* Each dependency should be present on its own line. - -### Action Scripts - -* Actions are defined in BASH scripts with the name `<action>.sh` within the platform directory. - -* The list of valid actions are: - - 1. `build`: Builds a set of binaries. - 2. `package`: Assembles the compiled binaries into a package. - 3. `sign`: Performs signing actions on a package. - 4. `publish`: Performs a publishing action for a package. - 5. `clean`: Cleans up any artifacts from the previous actions. - -* All package actions are optional, however at least one should generate output files, and any that do should contain a `clean` action. - -* Actions are executed in the order specified above, and later actions may depend on former actions. - -* Actions except for `clean` should `set -o errexit` to terminate on failed actions. - -* The `clean` action should always `exit 0` even if no work is done or it fails. - -* The `clean` action can be passed a variable as argument 1, named `keep_artifacts`, containing either the value `y` or `n`. It is indended to handle situations when the user runs `build --keep-artifacts` and should be handled intelligently. Usually, this is used to preserve Docker images while still removing temporary directories. - -### Output Files - -* Upon completion of the defined actions, at least one output file must be created in the `<platform>/pkg-dist` directory. - -* Output files will be moved to the directory `jellyfin-build/<platform>` one directory above the repository root upon completion. diff --git a/deployment/build.centos.amd64 b/deployment/build.centos.amd64 new file mode 100755 index 0000000000..939bbc45a4 --- /dev/null +++ b/deployment/build.centos.amd64 @@ -0,0 +1,24 @@ +#!/bin/bash + +#= CentOS/RHEL 7+ amd64 .rpm + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Build RPM +make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS +rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm + +# Move the artifacts out +mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +rm -f fedora/jellyfin*.tar.gz + +popd diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64 new file mode 100755 index 0000000000..f44c6a7d1d --- /dev/null +++ b/deployment/build.debian.amd64 @@ -0,0 +1,28 @@ +#!/bin/bash + +#= Debian 10+ amd64 .deb + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +if [[ ${IS_DOCKER} == YES ]]; then + # Remove build-dep for dotnet-sdk-3.1, since it's installed manually + cp -a debian/control /tmp/control.orig + sed -i '/dotnet-sdk-3.1,/d' debian/control +fi + +# Build DEB +dpkg-buildpackage -us -uc --pre-clean --post-clean + +mkdir -p ${ARTIFACT_DIR}/ +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + cp -a /tmp/control.orig debian/control + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64 new file mode 100755 index 0000000000..0127671f3d --- /dev/null +++ b/deployment/build.debian.arm64 @@ -0,0 +1,29 @@ +#!/bin/bash + +#= Debian 10+ arm64 .deb + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +if [[ ${IS_DOCKER} == YES ]]; then + # Remove build-dep for dotnet-sdk-3.1, since it's installed manually + cp -a debian/control /tmp/control.orig + sed -i '/dotnet-sdk-3.1,/d' debian/control +fi + +# Build DEB +export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} +dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean + +mkdir -p ${ARTIFACT_DIR}/ +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + cp -a /tmp/control.orig debian/control + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf new file mode 100755 index 0000000000..02e3db4fca --- /dev/null +++ b/deployment/build.debian.armhf @@ -0,0 +1,29 @@ +#!/bin/bash + +#= Debian 10+ arm64 .deb + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +if [[ ${IS_DOCKER} == YES ]]; then + # Remove build-dep for dotnet-sdk-3.1, since it's installed manually + cp -a debian/control /tmp/control.orig + sed -i '/dotnet-sdk-3.1,/d' debian/control +fi + +# Build DEB +export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} +dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean + +mkdir -p ${ARTIFACT_DIR}/ +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + cp -a /tmp/control.orig debian/control + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.fedora.amd64 b/deployment/build.fedora.amd64 new file mode 100755 index 0000000000..8ac99decc1 --- /dev/null +++ b/deployment/build.fedora.amd64 @@ -0,0 +1,24 @@ +#!/bin/bash + +#= Fedora 29+ amd64 .rpm + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Build RPM +make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS +rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm + +# Move the artifacts out +mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +rm -f fedora/jellyfin*.tar.gz + +popd diff --git a/deployment/build.linux.amd64 b/deployment/build.linux.amd64 new file mode 100755 index 0000000000..0cbbd05cf9 --- /dev/null +++ b/deployment/build.linux.amd64 @@ -0,0 +1,27 @@ +#!/bin/bash + +#= Generic Linux amd64 .tar.gz + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Get version +version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" + +# Build archives +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" +tar -czf jellyfin-server_${version}_linux-amd64.tar.gz -C dist jellyfin-server_${version} +rm -rf dist/jellyfin-server_${version} + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/ +mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.macos b/deployment/build.macos new file mode 100755 index 0000000000..16be29eeef --- /dev/null +++ b/deployment/build.macos @@ -0,0 +1,27 @@ +#!/bin/bash + +#= MacOS 10.13+ .tar.gz + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Get version +version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" + +# Build archives +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" +tar -czf jellyfin-server_${version}_macos-amd64.tar.gz -C dist jellyfin-server_${version} +rm -rf dist/jellyfin-server_${version} + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/ +mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.portable b/deployment/build.portable new file mode 100755 index 0000000000..1e8a4ab623 --- /dev/null +++ b/deployment/build.portable @@ -0,0 +1,27 @@ +#!/bin/bash + +#= Portable .NET DLL .tar.gz + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Get version +version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" + +# Build archives +dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" +tar -czf jellyfin-server_${version}_portable.tar.gz -C dist jellyfin-server_${version} +rm -rf dist/jellyfin-server_${version} + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/ +mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64 new file mode 100755 index 0000000000..107ddbe02d --- /dev/null +++ b/deployment/build.ubuntu.amd64 @@ -0,0 +1,28 @@ +#!/bin/bash + +#= Ubuntu 18.04+ amd64 .deb + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +if [[ ${IS_DOCKER} == YES ]]; then + # Remove build-dep for dotnet-sdk-3.1, since it's installed manually + cp -a debian/control /tmp/control.orig + sed -i '/dotnet-sdk-3.1,/d' debian/control +fi + +# Build DEB +dpkg-buildpackage -us -uc --pre-clean --post-clean + +mkdir -p ${ARTIFACT_DIR}/ +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + cp -a /tmp/control.orig debian/control + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64 new file mode 100755 index 0000000000..b13868f44b --- /dev/null +++ b/deployment/build.ubuntu.arm64 @@ -0,0 +1,29 @@ +#!/bin/bash + +#= Ubuntu 18.04+ arm64 .deb + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +if [[ ${IS_DOCKER} == YES ]]; then + # Remove build-dep for dotnet-sdk-3.1, since it's installed manually + cp -a debian/control /tmp/control.orig + sed -i '/dotnet-sdk-3.1,/d' debian/control +fi + +# Build DEB +export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} +dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean + +mkdir -p ${ARTIFACT_DIR}/ +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + cp -a /tmp/control.orig debian/control + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf new file mode 100755 index 0000000000..0b4dd308a2 --- /dev/null +++ b/deployment/build.ubuntu.armhf @@ -0,0 +1,29 @@ +#!/bin/bash + +#= Ubuntu 18.04+ arm64 .deb + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +if [[ ${IS_DOCKER} == YES ]]; then + # Remove build-dep for dotnet-sdk-3.1, since it's installed manually + cp -a debian/control /tmp/control.orig + sed -i '/dotnet-sdk-3.1,/d' debian/control +fi + +# Build DEB +export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} +dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean + +mkdir -p ${ARTIFACT_DIR}/ +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + cp -a /tmp/control.orig debian/control + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64 new file mode 100755 index 0000000000..39bd41f990 --- /dev/null +++ b/deployment/build.windows.amd64 @@ -0,0 +1,54 @@ +#!/bin/bash + +#= Windows 7+ amd64 (x64) .zip + +set -o errexit +set -o xtrace + +# Version variables +NSSM_VERSION="nssm-2.24-101-g897c7ad" +NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" +FFMPEG_VERSION="ffmpeg-4.2.1-win64-static" +FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip" + +# Move to source directory +pushd ${SOURCE_DIR} + +# Get version +version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" + +output_dir="dist/jellyfin-server_${version}" + +# Build binary +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" + +# Prepare addins +addin_build_dir="$( mktemp -d )" +wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip +wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip +unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir} +cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe ${output_dir}/nssm.exe +unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir} +cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${output_dir}/ffmpeg.exe +cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe ${output_dir}/ffprobe.exe +rm -rf ${addin_build_dir} + +# Prepare scripts +cp ${SOURCE_DIR}/windows/legacy/install-jellyfin.ps1 ${output_dir}/install-jellyfin.ps1 +cp ${SOURCE_DIR}/windows/legacy/install.bat ${output_dir}/install.bat + +# Create zip package +pushd dist +zip -qr jellyfin-server_${version}.portable.zip jellyfin-server_${version} +popd +rm -rf ${output_dir} + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/ +mv dist/jellyfin[-_]*.zip ${ARTIFACT_DIR}/ + +if [[ ${IS_DOCKER} == YES ]]; then + chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} +fi + +popd diff --git a/deployment/centos-package-x64/Dockerfile b/deployment/centos-package-x64/Dockerfile deleted file mode 100644 index 08219a2e4a..0000000000 --- a/deployment/centos-package-x64/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -FROM centos:7 -# Docker build arguments -ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/centos-package-x64 -ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=3.1 -# Docker run environment -ENV SOURCE_DIR=/jellyfin -ENV ARTIFACT_DIR=/dist - -# Prepare CentOS environment -RUN yum update -y \ - && yum install -y epel-release - -# Install build dependencies -RUN yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git - -# Install recent NodeJS and Yarn -RUN curl -fSsLo /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \ - && rpm -i https://rpm.nodesource.com/pub_10.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm \ - && yum install -y yarn - -# Install DotNET SDK -RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \ - && rpmdev-setuptree \ - && yum install -y dotnet-sdk-${SDK_VERSION} - -# Create symlinks and directories -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ - && mkdir -p ${SOURCE_DIR}/SPECS \ - && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ - && mkdir -p ${SOURCE_DIR}/SOURCES \ - && ln -s ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/SOURCES - -VOLUME ${ARTIFACT_DIR}/ - -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/centos-package-x64/clean.sh b/deployment/centos-package-x64/clean.sh deleted file mode 100755 index 31455de0d4..0000000000 --- a/deployment/centos-package-x64/clean.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" -VERSION="$( grep -A1 '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -package_source_dir="${WORKDIR}/pkg-src" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-centos-build" - -rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null \ - || sudo rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/centos-package-x64/dependencies.txt b/deployment/centos-package-x64/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/centos-package-x64/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/centos-package-x64/docker-build.sh b/deployment/centos-package-x64/docker-build.sh deleted file mode 100755 index 62dd144e50..0000000000 --- a/deployment/centos-package-x64/docker-build.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Builds the RPM inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Build RPM -make -f .copr/Makefile srpm outdir=/root/rpmbuild/SRPMS -rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/rpm -mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/centos-package-x64/package.sh b/deployment/centos-package-x64/package.sh deleted file mode 100755 index 1b983f49d9..0000000000 --- a/deployment/centos-package-x64/package.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-centos-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile -# Build the RPMs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the RPMs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/rpm/* "${output_dir}" diff --git a/deployment/centos-package-x64/pkg-src b/deployment/centos-package-x64/pkg-src deleted file mode 120000 index 3ff4d3cbf5..0000000000 --- a/deployment/centos-package-x64/pkg-src +++ /dev/null @@ -1 +0,0 @@ -../fedora-package-x64/pkg-src/ \ No newline at end of file diff --git a/deployment/debian-package-arm64/Dockerfile.arm64 b/deployment/debian-package-arm64/Dockerfile.arm64 deleted file mode 100644 index 9ca4868441..0000000000 --- a/deployment/debian-package-arm64/Dockerfile.arm64 +++ /dev/null @@ -1,34 +0,0 @@ -FROM debian:10 -# Docker build arguments -ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64 -ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=3.1 -# Docker run environment -ENV SOURCE_DIR=/jellyfin -ENV ARTIFACT_DIR=/dist -ENV DEB_BUILD_OPTIONS=noddebs -ENV ARCH=arm64 - -# Prepare Debian build environment -RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0 - -# Install dotnet repository -# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5a4c8f96-1c73-401c-a6de-8e100403188a/0ce6ab39747e2508366d498f9c0a0669/dotnet-sdk-3.1.100-linux-arm64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh - -# Link to Debian source dir; mkdir needed or it fails, can't force dest -RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian - -VOLUME ${ARTIFACT_DIR}/ - -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/debian-package-arm64/clean.sh b/deployment/debian-package-arm64/clean.sh deleted file mode 100755 index e7bfdf8b4b..0000000000 --- a/deployment/debian-package-arm64/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-debian_arm64-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/debian-package-arm64/dependencies.txt b/deployment/debian-package-arm64/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/debian-package-arm64/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/debian-package-arm64/docker-build.sh b/deployment/debian-package-arm64/docker-build.sh deleted file mode 100755 index 67ab6bd74b..0000000000 --- a/deployment/debian-package-arm64/docker-build.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Builds the DEB inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image -sed -i '/dotnet-sdk-3.1,/d' debian/control - -# Build DEB -export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} -dpkg-buildpackage -us -uc -aarm64 - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/deb -mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/debian-package-arm64/package.sh b/deployment/debian-package-arm64/package.sh deleted file mode 100755 index 2091982187..0000000000 --- a/deployment/debian-package-arm64/package.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -ARCH="$( arch )" -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-debian_arm64-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Determine which Dockerfile to use -case $ARCH in - 'x86_64') - DOCKERFILE="Dockerfile.amd64" - ;; - 'armv7l') - DOCKERFILE="Dockerfile.arm64" - ;; -esac - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-arm64/pkg-src b/deployment/debian-package-arm64/pkg-src deleted file mode 120000 index 4c695fea17..0000000000 --- a/deployment/debian-package-arm64/pkg-src +++ /dev/null @@ -1 +0,0 @@ -../debian-package-x64/pkg-src \ No newline at end of file diff --git a/deployment/debian-package-armhf/Dockerfile.armhf b/deployment/debian-package-armhf/Dockerfile.armhf deleted file mode 100644 index dd398b5aa5..0000000000 --- a/deployment/debian-package-armhf/Dockerfile.armhf +++ /dev/null @@ -1,34 +0,0 @@ -FROM debian:10 -# Docker build arguments -ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf -ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=3.1 -# Docker run environment -ENV SOURCE_DIR=/jellyfin -ENV ARTIFACT_DIR=/dist -ENV DEB_BUILD_OPTIONS=noddebs -ENV ARCH=armhf - -# Prepare Debian build environment -RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0 - -# Install dotnet repository -# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/67766a96-eb8c-4cd2-bca4-ea63d2cc115c/7bf13840aa2ed88793b7315d5e0d74e6/dotnet-sdk-3.1.100-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh - -# Link to Debian source dir; mkdir needed or it fails, can't force dest -RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian - -VOLUME ${ARTIFACT_DIR}/ - -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/debian-package-armhf/clean.sh b/deployment/debian-package-armhf/clean.sh deleted file mode 100755 index 35a3d3e9ad..0000000000 --- a/deployment/debian-package-armhf/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-debian_armhf-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/debian-package-armhf/dependencies.txt b/deployment/debian-package-armhf/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/debian-package-armhf/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/debian-package-armhf/docker-build.sh b/deployment/debian-package-armhf/docker-build.sh deleted file mode 100755 index 1bd7fb2911..0000000000 --- a/deployment/debian-package-armhf/docker-build.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Builds the DEB inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image -sed -i '/dotnet-sdk-3.1,/d' debian/control - -# Build DEB -export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} -dpkg-buildpackage -us -uc -aarmhf - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/deb -mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/debian-package-armhf/package.sh b/deployment/debian-package-armhf/package.sh deleted file mode 100755 index 4a27dd8283..0000000000 --- a/deployment/debian-package-armhf/package.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -ARCH="$( arch )" -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-debian_armhf-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Determine which Dockerfile to use -case $ARCH in - 'x86_64') - DOCKERFILE="Dockerfile.amd64" - ;; - 'armv7l') - DOCKERFILE="Dockerfile.armhf" - ;; -esac - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-armhf/pkg-src b/deployment/debian-package-armhf/pkg-src deleted file mode 120000 index 0bb6d55249..0000000000 --- a/deployment/debian-package-armhf/pkg-src +++ /dev/null @@ -1 +0,0 @@ -../debian-package-x64/pkg-src/ \ No newline at end of file diff --git a/deployment/debian-package-x64/Dockerfile b/deployment/debian-package-x64/Dockerfile deleted file mode 100644 index e863d1edf9..0000000000 --- a/deployment/debian-package-x64/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM debian:10 -# Docker build arguments -ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-x64 -ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=3.1 -# Docker run environment -ENV SOURCE_DIR=/jellyfin -ENV ARTIFACT_DIR=/dist -ENV DEB_BUILD_OPTIONS=noddebs -ENV ARCH=amd64 - -# Prepare Debian build environment -RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget npm devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 - -# Install dotnet repository -# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh - -# Link to Debian source dir; mkdir needed or it fails, can't force dest -RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian - -VOLUME ${ARTIFACT_DIR}/ - -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/debian-package-x64/clean.sh b/deployment/debian-package-x64/clean.sh deleted file mode 100755 index 4e507bcb27..0000000000 --- a/deployment/debian-package-x64/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-debian-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/debian-package-x64/dependencies.txt b/deployment/debian-package-x64/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/debian-package-x64/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/debian-package-x64/docker-build.sh b/deployment/debian-package-x64/docker-build.sh deleted file mode 100755 index 962a522ebc..0000000000 --- a/deployment/debian-package-x64/docker-build.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# Builds the DEB inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image -sed -i '/dotnet-sdk-3.1,/d' debian/control - -# Build DEB -dpkg-buildpackage -us -uc - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/deb -mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/debian-package-x64/package.sh b/deployment/debian-package-x64/package.sh deleted file mode 100755 index 5a416959ab..0000000000 --- a/deployment/debian-package-x64/package.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-debian-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/fedora-package-x64/clean.sh b/deployment/fedora-package-x64/clean.sh deleted file mode 100755 index 700c8f1bb3..0000000000 --- a/deployment/fedora-package-x64/clean.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" -VERSION="$( grep -A1 '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -package_source_dir="${WORKDIR}/pkg-src" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-fedora-build" - -rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null \ - || sudo rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/fedora-package-x64/dependencies.txt b/deployment/fedora-package-x64/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/fedora-package-x64/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/fedora-package-x64/docker-build.sh b/deployment/fedora-package-x64/docker-build.sh deleted file mode 100755 index 740e8d35ca..0000000000 --- a/deployment/fedora-package-x64/docker-build.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Builds the RPM inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Build RPM -make -f .copr/Makefile srpm outdir=/root/rpmbuild/SRPMS -rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/rpm -mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/fedora-package-x64/package.sh b/deployment/fedora-package-x64/package.sh deleted file mode 100755 index ae6962dd1f..0000000000 --- a/deployment/fedora-package-x64/package.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-fedora-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile -# Build the RPMs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the RPMs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/rpm/* "${output_dir}" diff --git a/deployment/linux-x64/clean.sh b/deployment/linux-x64/clean.sh deleted file mode 100755 index c07501a7bb..0000000000 --- a/deployment/linux-x64/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-linux-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/linux-x64/dependencies.txt b/deployment/linux-x64/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/linux-x64/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/linux-x64/docker-build.sh b/deployment/linux-x64/docker-build.sh deleted file mode 100755 index e33328a36a..0000000000 --- a/deployment/linux-x64/docker-build.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Builds the TAR archive inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Clone down and build Web frontend -web_build_dir="$( mktemp -d )" -web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" -git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ -pushd ${web_build_dir} -if [[ -n ${web_branch} ]]; then - checkout -b origin/${web_branch} -fi -yarn install -mkdir -p ${web_target} -mv dist/* ${web_target}/ -popd -rm -rf ${web_build_dir} - -# Get version -version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" - -# Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" -tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version} -rm -rf /dist/jellyfin_${version} - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/linux-x64/package.sh b/deployment/linux-x64/package.sh deleted file mode 100755 index dfe8a9aa4a..0000000000 --- a/deployment/linux-x64/package.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-linux-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/macos/clean.sh b/deployment/macos/clean.sh deleted file mode 100755 index c07501a7bb..0000000000 --- a/deployment/macos/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-linux-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/macos/dependencies.txt b/deployment/macos/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/macos/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/macos/docker-build.sh b/deployment/macos/docker-build.sh deleted file mode 100755 index f9417388d7..0000000000 --- a/deployment/macos/docker-build.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Builds the TAR archive inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Clone down and build Web frontend -web_build_dir="$( mktemp -d )" -web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" -git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ -pushd ${web_build_dir} -if [[ -n ${web_branch} ]]; then - checkout -b origin/${web_branch} -fi -yarn install -mkdir -p ${web_target} -mv dist/* ${web_target}/ -popd -rm -rf ${web_build_dir} - -# Get version -version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" - -# Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" -tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version} -rm -rf /dist/jellyfin_${version} - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/macos/package.sh b/deployment/macos/package.sh deleted file mode 100755 index 464c0d382f..0000000000 --- a/deployment/macos/package.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-macos-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/portable/clean.sh b/deployment/portable/clean.sh deleted file mode 100755 index c07501a7bb..0000000000 --- a/deployment/portable/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-linux-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/portable/dependencies.txt b/deployment/portable/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/portable/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/portable/docker-build.sh b/deployment/portable/docker-build.sh deleted file mode 100755 index 094190bbf6..0000000000 --- a/deployment/portable/docker-build.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Builds the TAR archive inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Clone down and build Web frontend -web_build_dir="$( mktemp -d )" -web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" -git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ -pushd ${web_build_dir} -if [[ -n ${web_branch} ]]; then - checkout -b origin/${web_branch} -fi -yarn install -mkdir -p ${web_target} -mv dist/* ${web_target}/ -popd -rm -rf ${web_build_dir} - -# Get version -version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" - -# Build archives -dotnet publish Jellyfin.Server --configuration Release --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" -tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version} -rm -rf /dist/jellyfin_${version} - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/portable/package.sh b/deployment/portable/package.sh deleted file mode 100755 index 0ceb54dda1..0000000000 --- a/deployment/portable/package.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-portable-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/ubuntu-package-arm64/Dockerfile.arm64 b/deployment/ubuntu-package-arm64/Dockerfile.arm64 deleted file mode 100644 index 8f004b2f1a..0000000000 --- a/deployment/ubuntu-package-arm64/Dockerfile.arm64 +++ /dev/null @@ -1,40 +0,0 @@ -FROM ubuntu:bionic -# Docker build arguments -ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-arm64 -ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=3.1 -# Docker run environment -ENV SOURCE_DIR=/jellyfin -ENV ARTIFACT_DIR=/dist -ENV DEB_BUILD_OPTIONS=noddebs -ENV ARCH=arm64 - -# Prepare Debian build environment -RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 libssl-dev - -# Install dotnet repository -# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5a4c8f96-1c73-401c-a6de-8e100403188a/0ce6ab39747e2508366d498f9c0a0669/dotnet-sdk-3.1.100-linux-arm64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - -# Install npm package manager -RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ - && apt update \ - && apt install -y nodejs - -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh - -# Link to Debian source dir; mkdir needed or it fails, can't force dest -RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian - -VOLUME ${ARTIFACT_DIR}/ - -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/ubuntu-package-arm64/clean.sh b/deployment/ubuntu-package-arm64/clean.sh deleted file mode 100755 index 82d427f9e5..0000000000 --- a/deployment/ubuntu-package-arm64/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-ubuntu-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/ubuntu-package-arm64/dependencies.txt b/deployment/ubuntu-package-arm64/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/ubuntu-package-arm64/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/ubuntu-package-arm64/docker-build.sh b/deployment/ubuntu-package-arm64/docker-build.sh deleted file mode 100755 index 67ab6bd74b..0000000000 --- a/deployment/ubuntu-package-arm64/docker-build.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Builds the DEB inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image -sed -i '/dotnet-sdk-3.1,/d' debian/control - -# Build DEB -export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} -dpkg-buildpackage -us -uc -aarm64 - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/deb -mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/ubuntu-package-arm64/package.sh b/deployment/ubuntu-package-arm64/package.sh deleted file mode 100755 index d1140a7274..0000000000 --- a/deployment/ubuntu-package-arm64/package.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -ARCH="$( arch )" -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-ubuntu_arm64-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Determine which Dockerfile to use -case $ARCH in - 'x86_64') - DOCKERFILE="Dockerfile.amd64" - ;; - 'armv7l') - DOCKERFILE="Dockerfile.arm64" - ;; -esac - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/ubuntu-package-arm64/pkg-src b/deployment/ubuntu-package-arm64/pkg-src deleted file mode 120000 index 4c695fea17..0000000000 --- a/deployment/ubuntu-package-arm64/pkg-src +++ /dev/null @@ -1 +0,0 @@ -../debian-package-x64/pkg-src \ No newline at end of file diff --git a/deployment/ubuntu-package-armhf/Dockerfile.armhf b/deployment/ubuntu-package-armhf/Dockerfile.armhf deleted file mode 100644 index 0e71fa6938..0000000000 --- a/deployment/ubuntu-package-armhf/Dockerfile.armhf +++ /dev/null @@ -1,40 +0,0 @@ -FROM ubuntu:bionic -# Docker build arguments -ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-armhf -ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=3.1 -# Docker run environment -ENV SOURCE_DIR=/jellyfin -ENV ARTIFACT_DIR=/dist -ENV DEB_BUILD_OPTIONS=noddebs -ENV ARCH=armhf - -# Prepare Debian build environment -RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 libssl-dev - -# Install dotnet repository -# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/67766a96-eb8c-4cd2-bca4-ea63d2cc115c/7bf13840aa2ed88793b7315d5e0d74e6/dotnet-sdk-3.1.100-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - -# Install npm package manager -RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ - && apt update \ - && apt install -y nodejs - -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh - -# Link to Debian source dir; mkdir needed or it fails, can't force dest -RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian - -VOLUME ${ARTIFACT_DIR}/ - -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/ubuntu-package-armhf/clean.sh b/deployment/ubuntu-package-armhf/clean.sh deleted file mode 100755 index 82d427f9e5..0000000000 --- a/deployment/ubuntu-package-armhf/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-ubuntu-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/ubuntu-package-armhf/dependencies.txt b/deployment/ubuntu-package-armhf/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/ubuntu-package-armhf/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/ubuntu-package-armhf/docker-build.sh b/deployment/ubuntu-package-armhf/docker-build.sh deleted file mode 100755 index 1bd7fb2911..0000000000 --- a/deployment/ubuntu-package-armhf/docker-build.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Builds the DEB inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image -sed -i '/dotnet-sdk-3.1,/d' debian/control - -# Build DEB -export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} -dpkg-buildpackage -us -uc -aarmhf - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/deb -mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/ubuntu-package-armhf/package.sh b/deployment/ubuntu-package-armhf/package.sh deleted file mode 100755 index 2ceb3e8165..0000000000 --- a/deployment/ubuntu-package-armhf/package.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -ARCH="$( arch )" -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-ubuntu_armhf-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Determine which Dockerfile to use -case $ARCH in - 'x86_64') - DOCKERFILE="Dockerfile.amd64" - ;; - 'armv7l') - DOCKERFILE="Dockerfile.armhf" - ;; -esac - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/ubuntu-package-armhf/pkg-src b/deployment/ubuntu-package-armhf/pkg-src deleted file mode 120000 index 4c695fea17..0000000000 --- a/deployment/ubuntu-package-armhf/pkg-src +++ /dev/null @@ -1 +0,0 @@ -../debian-package-x64/pkg-src \ No newline at end of file diff --git a/deployment/ubuntu-package-x64/Dockerfile b/deployment/ubuntu-package-x64/Dockerfile deleted file mode 100644 index e2dda6392c..0000000000 --- a/deployment/ubuntu-package-x64/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -FROM ubuntu:bionic -# Docker build arguments -ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-x64 -ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=3.1 -# Docker run environment -ENV SOURCE_DIR=/jellyfin -ENV ARTIFACT_DIR=/dist -ENV DEB_BUILD_OPTIONS=noddebs -ENV ARCH=amd64 - -# Prepare Ubuntu build environment -RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0 \ - && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ - && mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian - -# Install dotnet repository -# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - -# Install npm package manager -RUN wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \ - && echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -s -c) main" > /etc/apt/sources.list.d/npm.list \ - && apt update \ - && apt install -y nodejs - -VOLUME ${ARTIFACT_DIR}/ - -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/ubuntu-package-x64/clean.sh b/deployment/ubuntu-package-x64/clean.sh deleted file mode 100755 index 82d427f9e5..0000000000 --- a/deployment/ubuntu-package-x64/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-ubuntu-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/ubuntu-package-x64/dependencies.txt b/deployment/ubuntu-package-x64/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/ubuntu-package-x64/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/ubuntu-package-x64/docker-build.sh b/deployment/ubuntu-package-x64/docker-build.sh deleted file mode 100755 index 962a522ebc..0000000000 --- a/deployment/ubuntu-package-x64/docker-build.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# Builds the DEB inside the Docker container - -set -o errexit -set -o xtrace - -# Move to source directory -pushd ${SOURCE_DIR} - -# Remove build-dep for dotnet-sdk-3.1, since it's not a package in this image -sed -i '/dotnet-sdk-3.1,/d' debian/control - -# Build DEB -dpkg-buildpackage -us -uc - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/deb -mv /jellyfin[-_]* ${ARTIFACT_DIR}/deb/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/ubuntu-package-x64/package.sh b/deployment/ubuntu-package-x64/package.sh deleted file mode 100755 index 08c003778b..0000000000 --- a/deployment/ubuntu-package-x64/package.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-ubuntu-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/ubuntu-package-x64/pkg-src b/deployment/ubuntu-package-x64/pkg-src deleted file mode 120000 index 0bb6d55249..0000000000 --- a/deployment/ubuntu-package-x64/pkg-src +++ /dev/null @@ -1 +0,0 @@ -../debian-package-x64/pkg-src/ \ No newline at end of file diff --git a/deployment/win-x64/clean.sh b/deployment/win-x64/clean.sh deleted file mode 100755 index 6c183f3371..0000000000 --- a/deployment/win-x64/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-windows-x64-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/win-x64/dependencies.txt b/deployment/win-x64/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/win-x64/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/win-x64/docker-build.sh b/deployment/win-x64/docker-build.sh deleted file mode 100755 index 79e5fb0bcd..0000000000 --- a/deployment/win-x64/docker-build.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -# Builds the ZIP archive inside the Docker container - -set -o errexit -set -o xtrace - -# Version variables -NSSM_VERSION="nssm-2.24-101-g897c7ad" -NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" -FFMPEG_VERSION="ffmpeg-4.2.1-win64-static" -FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip" - -# Move to source directory -pushd ${SOURCE_DIR} - -# Clone down and build Web frontend -web_build_dir="$( mktemp -d )" -web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" -git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ -pushd ${web_build_dir} -if [[ -n ${web_branch} ]]; then - checkout -b origin/${web_branch} -fi -yarn install -mkdir -p ${web_target} -mv dist/* ${web_target}/ -popd -rm -rf ${web_build_dir} - -# Get version -version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" - -# Build binary -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" - -# Prepare addins -addin_build_dir="$( mktemp -d )" -wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip -wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip -unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir} -cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe /dist/jellyfin_${version}/nssm.exe -unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir} -cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe /dist/jellyfin_${version}/ffmpeg.exe -cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe /dist/jellyfin_${version}/ffprobe.exe -rm -rf ${addin_build_dir} - -# Prepare scripts -cp ${SOURCE_DIR}/deployment/windows/legacy/install-jellyfin.ps1 /dist/jellyfin_${version}/install-jellyfin.ps1 -cp ${SOURCE_DIR}/deployment/windows/legacy/install.bat /dist/jellyfin_${version}/install.bat - -# Create zip package -pushd /dist -zip -r /jellyfin_${version}.portable.zip jellyfin_${version} -popd -rm -rf /dist/jellyfin_${version} - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv /jellyfin[-_]*.zip ${ARTIFACT_DIR}/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/win-x64/package.sh b/deployment/win-x64/package.sh deleted file mode 100755 index a8ab190fa5..0000000000 --- a/deployment/win-x64/package.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-windows-x64-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/win-x86/Dockerfile b/deployment/win-x86/Dockerfile deleted file mode 100644 index f8dc5be83d..0000000000 --- a/deployment/win-x86/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM debian:10 -# Docker build arguments -ARG SOURCE_DIR=/jellyfin -ARG PLATFORM_DIR=/jellyfin/deployment/win-x86 -ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=3.1 -# Docker run environment -ENV SOURCE_DIR=/jellyfin -ENV ARTIFACT_DIR=/dist -ENV DEB_BUILD_OPTIONS=noddebs -ENV ARCH=amd64 - -# Prepare Debian build environment -RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip - -# Install dotnet repository -# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/d731f991-8e68-4c7c-8ea0-fad5605b077a/49497b5420eecbd905158d86d738af64/dotnet-sdk-3.1.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - -# Install yarn package manager -RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ - && apt update \ - && apt install -y yarn - -# Link to docker-build script -RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh - -VOLUME ${ARTIFACT_DIR}/ - -COPY . ${SOURCE_DIR}/ - -ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/win-x86/clean.sh b/deployment/win-x86/clean.sh deleted file mode 100755 index 8b78c5e4b6..0000000000 --- a/deployment/win-x86/clean.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -keep_artifacts="${1}" - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-windows-x86-build" - -rm -rf "${package_temporary_dir}" &>/dev/null \ - || sudo rm -rf "${package_temporary_dir}" &>/dev/null - -rm -rf "${output_dir}" &>/dev/null \ - || sudo rm -rf "${output_dir}" &>/dev/null - -if [[ ${keep_artifacts} == 'n' ]]; then - docker_sudo="" - if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo=sudo - fi - ${docker_sudo} docker image rm ${image_name} --force -fi diff --git a/deployment/win-x86/dependencies.txt b/deployment/win-x86/dependencies.txt deleted file mode 100644 index bdb9670965..0000000000 --- a/deployment/win-x86/dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -docker diff --git a/deployment/win-x86/docker-build.sh b/deployment/win-x86/docker-build.sh deleted file mode 100755 index 977dcf78fa..0000000000 --- a/deployment/win-x86/docker-build.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -# Builds the ZIP archive inside the Docker container - -set -o errexit -set -o xtrace - -# Version variables -NSSM_VERSION="nssm-2.24-101-g897c7ad" -NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" -FFMPEG_VERSION="ffmpeg-4.2.1-win32-static" -FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip" - -# Move to source directory -pushd ${SOURCE_DIR} - -# Clone down and build Web frontend -web_build_dir="$( mktemp -d )" -web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" -git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ -pushd ${web_build_dir} -if [[ -n ${web_branch} ]]; then - checkout -b origin/${web_branch} -fi -yarn install -mkdir -p ${web_target} -mv dist/* ${web_target}/ -popd -rm -rf ${web_build_dir} - -# Get version -version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" - -# Build binary -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x86 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" - -# Prepare addins -addin_build_dir="$( mktemp -d )" -wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip -wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip -unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir} -cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe /dist/jellyfin_${version}/nssm.exe -unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir} -cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe /dist/jellyfin_${version}/ffmpeg.exe -cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe /dist/jellyfin_${version}/ffprobe.exe -rm -rf ${addin_build_dir} - -# Prepare scripts -cp ${SOURCE_DIR}/deployment/windows/legacy/install-jellyfin.ps1 /dist/jellyfin_${version}/install-jellyfin.ps1 -cp ${SOURCE_DIR}/deployment/windows/legacy/install.bat /dist/jellyfin_${version}/install.bat - -# Create zip package -pushd /dist -zip -r /jellyfin_${version}.portable.zip jellyfin_${version} -popd -rm -rf /dist/jellyfin_${version} - -# Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv /jellyfin[-_]*.zip ${ARTIFACT_DIR}/ -chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/win-x86/package.sh b/deployment/win-x86/package.sh deleted file mode 100755 index 65e7e2928c..0000000000 --- a/deployment/win-x86/package.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -args="${@}" -declare -a docker_envvars -for arg in ${args}; do - docker_envvars+=("-e ${arg}") -done - -WORKDIR="$( pwd )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -output_dir="${WORKDIR}/pkg-dist" -current_user="$( whoami )" -image_name="jellyfin-windows-x86-build" - -# Determine if sudo should be used for Docker -if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ - && [[ ! ${EUID:-1000} -eq 0 ]] \ - && [[ ! ${USER} == "root" ]] \ - && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then - docker_sudo="sudo" -else - docker_sudo="" -fi - -# Prepare temporary package dir -mkdir -p "${package_temporary_dir}" -# Set up the build environment Docker image -${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile -# Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} -# Move the DEBs to the output directory -mkdir -p "${output_dir}" -mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/fedora-package-x64/pkg-src/.gitignore b/fedora/.gitignore similarity index 100% rename from deployment/fedora-package-x64/pkg-src/.gitignore rename to fedora/.gitignore diff --git a/fedora/Makefile b/fedora/Makefile new file mode 100644 index 0000000000..97904ddd35 --- /dev/null +++ b/fedora/Makefile @@ -0,0 +1,26 @@ +VERSION := $(shell sed -ne '/^Version:/s/.* *//p' fedora/jellyfin.spec) + +srpm: + cd fedora/; \ + SOURCE_DIR=.. \ + WORKDIR="$${PWD}"; \ + tar \ + --transform "s,^\.,jellyfin-server-$(VERSION)," \ + --exclude='.git*' \ + --exclude='**/.git' \ + --exclude='**/.hg' \ + --exclude='**/.vs' \ + --exclude='**/.vscode' \ + --exclude='deployment' \ + --exclude='**/bin' \ + --exclude='**/obj' \ + --exclude='**/.nuget' \ + --exclude='*.deb' \ + --exclude='*.rpm' \ + --exclude='jellyfin-server-$(VERSION).tar.gz' \ + -czf "jellyfin-server-$(VERSION).tar.gz" \ + -C $${SOURCE_DIR} ./ + cd fedora/; \ + rpmbuild -bs jellyfin.spec \ + --define "_sourcedir $$PWD/" \ + --define "_srcrpmdir $(outdir)" diff --git a/deployment/fedora-package-x64/pkg-src/README.md b/fedora/README.md similarity index 100% rename from deployment/fedora-package-x64/pkg-src/README.md rename to fedora/README.md diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin-firewalld.xml b/fedora/jellyfin-firewalld.xml similarity index 100% rename from deployment/fedora-package-x64/pkg-src/jellyfin-firewalld.xml rename to fedora/jellyfin-firewalld.xml diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.env b/fedora/jellyfin.env similarity index 88% rename from deployment/fedora-package-x64/pkg-src/jellyfin.env rename to fedora/jellyfin.env index de48f13af5..bf64acd3f9 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.env +++ b/fedora/jellyfin.env @@ -20,6 +20,9 @@ JELLYFIN_CONFIG_DIR="/etc/jellyfin" JELLYFIN_LOG_DIR="/var/log/jellyfin" JELLYFIN_CACHE_DIR="/var/cache/jellyfin" +# web client path, installed by the jellyfin-web package +JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web" + # In-App service control JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh" diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.override.conf b/fedora/jellyfin.override.conf similarity index 100% rename from deployment/fedora-package-x64/pkg-src/jellyfin.override.conf rename to fedora/jellyfin.override.conf diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.service b/fedora/jellyfin.service similarity index 69% rename from deployment/fedora-package-x64/pkg-src/jellyfin.service rename to fedora/jellyfin.service index f3dc594b1c..b092ebf2f0 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.service +++ b/fedora/jellyfin.service @@ -5,7 +5,7 @@ Description=Jellyfin is a free software media system that puts you in control of [Service] EnvironmentFile=/etc/sysconfig/jellyfin WorkingDirectory=/var/lib/jellyfin -ExecStart=/usr/bin/jellyfin ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} +ExecStart=/usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} TimeoutSec=15 Restart=on-failure User=jellyfin diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/fedora/jellyfin.spec similarity index 62% rename from deployment/fedora-package-x64/pkg-src/jellyfin.spec rename to fedora/jellyfin.spec index 33c6f6f648..9311864a63 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -7,15 +7,13 @@ %endif Name: jellyfin -Version: 10.5.0 +Version: 10.6.0 Release: 1%{?dist} -Summary: The Free Software Media Browser -License: GPLv2 -URL: https://jellyfin.media +Summary: The Free Software Media System +License: GPLv3 +URL: https://jellyfin.org # Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz` -Source0: https://github.com/%{name}/%{name}/archive/%{name}-%{version}.tar.gz -# Jellyfin Webinterface downloaded by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz` -Source1: https://github.com/%{name}/%{name}-web/archive/%{name}-web-%{version}.tar.gz +Source0: jellyfin-server-%{version}.tar.gz Source11: jellyfin.service Source12: jellyfin.env Source13: jellyfin.sudoers @@ -25,43 +23,30 @@ Source16: jellyfin-firewalld.xml %{?systemd_requires} BuildRequires: systemd -Requires(pre): shadow-utils -BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel, git -%if 0%{?fedora} -BuildRequires: nodejs-yarn, git -%else -# Requirements not packaged in main repos -# From https://rpm.nodesource.com/pub_10.x/el/7/x86_64/ -BuildRequires: nodejs >= 10 yarn -%endif -Requires: libcurl, fontconfig, freetype, openssl, glibc libicu +BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel # Requirements not packaged in main repos # COPR @dotnet-sig/dotnet or # https://packages.microsoft.com/rhel/7/prod/ BuildRequires: dotnet-runtime-3.1, dotnet-sdk-3.1 -# RPMfusion free -Requires: ffmpeg - +Requires: %{name}-server = %{version}-%{release}, %{name}-web >= 10.6, %{name}-web < 10.7 # Disable Automatic Dependency Processing AutoReqProv: no %description Jellyfin is a free software media system that puts you in control of managing and streaming your media. +%package server +# RPMfusion free +Summary: The Free Software Media System Server backend +Requires(pre): shadow-utils +Requires: ffmpeg +Requires: libcurl, fontconfig, freetype, openssl, glibc libicu + +%description server +The Jellyfin media server backend. %prep -%autosetup -n %{name}-%{version} -b 0 -b 1 -web_build_dir="$(mktemp -d)" -web_target="$PWD/MediaBrowser.WebDashboard/jellyfin-web" -pushd ../jellyfin-web-%{version} || pushd ../jellyfin-web-master -%if 0%{?fedora} -nodejs-yarn install -%else -yarn install -%endif -mkdir -p ${web_target} -mv dist/* ${web_target}/ -popd +%autosetup -n jellyfin-server-%{version} -b 0 %build @@ -70,81 +55,76 @@ export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server -%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE -%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf -%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json +%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE +%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf +%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json %{__mkdir} -p %{buildroot}%{_bindir} tee %{buildroot}%{_bindir}/jellyfin << EOF #!/bin/sh -exec %{_libdir}/%{name}/%{name} \${@} +exec %{_libdir}/jellyfin/jellyfin \${@} EOF %{__mkdir} -p %{buildroot}%{_sharedstatedir}/jellyfin -%{__mkdir} -p %{buildroot}%{_sysconfdir}/%{name} +%{__mkdir} -p %{buildroot}%{_sysconfdir}/jellyfin %{__mkdir} -p %{buildroot}%{_var}/log/jellyfin %{__mkdir} -p %{buildroot}%{_var}/cache/jellyfin -%{__install} -D -m 0644 %{SOURCE11} %{buildroot}%{_unitdir}/%{name}.service -%{__install} -D -m 0644 %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/%{name} -%{__install} -D -m 0600 %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/%{name}-sudoers -%{__install} -D -m 0755 %{SOURCE14} %{buildroot}%{_libexecdir}/%{name}/restart.sh -%{__install} -D -m 0644 %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/%{name}.xml +%{__install} -D -m 0644 %{SOURCE11} %{buildroot}%{_unitdir}/jellyfin.service +%{__install} -D -m 0644 %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/jellyfin +%{__install} -D -m 0600 %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/jellyfin-sudoers +%{__install} -D -m 0755 %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh +%{__install} -D -m 0644 %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml -%files -%{_libdir}/%{name}/jellyfin-web/* -%attr(755,root,root) %{_bindir}/%{name} -%{_libdir}/%{name}/*.json -%{_libdir}/%{name}/*.dll -%{_libdir}/%{name}/*.so -%{_libdir}/%{name}/*.a -%{_libdir}/%{name}/createdump +%files server +%attr(755,root,root) %{_bindir}/jellyfin +%{_libdir}/jellyfin/*.json +%{_libdir}/jellyfin/*.dll +%{_libdir}/jellyfin/*.so +%{_libdir}/jellyfin/*.a +%{_libdir}/jellyfin/createdump # Needs 755 else only root can run it since binary build by dotnet is 722 -%attr(755,root,root) %{_libdir}/%{name}/jellyfin -%{_libdir}/%{name}/SOS_README.md -%{_unitdir}/%{name}.service -%{_libexecdir}/%{name}/restart.sh -%{_prefix}/lib/firewalld/services/%{name}.xml -%attr(755,jellyfin,jellyfin) %dir %{_sysconfdir}/%{name} -%config %{_sysconfdir}/sysconfig/%{name} -%config(noreplace) %attr(600,root,root) %{_sysconfdir}/sudoers.d/%{name}-sudoers -%config(noreplace) %{_sysconfdir}/systemd/system/%{name}.service.d/override.conf -%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/%{name}/logging.json +%attr(755,root,root) %{_libdir}/jellyfin/jellyfin +%{_libdir}/jellyfin/SOS_README.md +%{_unitdir}/jellyfin.service +%{_libexecdir}/jellyfin/restart.sh +%{_prefix}/lib/firewalld/services/jellyfin.xml +%attr(755,jellyfin,jellyfin) %dir %{_sysconfdir}/jellyfin +%config %{_sysconfdir}/sysconfig/jellyfin +%config(noreplace) %attr(600,root,root) %{_sysconfdir}/sudoers.d/jellyfin-sudoers +%config(noreplace) %{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf +%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json %attr(750,jellyfin,jellyfin) %dir %{_sharedstatedir}/jellyfin %attr(-,jellyfin,jellyfin) %dir %{_var}/log/jellyfin %attr(750,jellyfin,jellyfin) %dir %{_var}/cache/jellyfin -%if 0%{?fedora} -%license LICENSE -%else -%{_datadir}/licenses/%{name}/LICENSE -%endif +%{_datadir}/licenses/jellyfin/LICENSE -%pre +%pre server getent group jellyfin >/dev/null || groupadd -r jellyfin getent passwd jellyfin >/dev/null || \ useradd -r -g jellyfin -d %{_sharedstatedir}/jellyfin -s /sbin/nologin \ -c "Jellyfin default user" jellyfin exit 0 -%post +%post server # Move existing configuration cache and logs to their new locations and symlink them. if [ $1 -gt 1 ] ; then service_state=$(systemctl is-active jellyfin.service) if [ "${service_state}" = "active" ]; then systemctl stop jellyfin.service fi - if [ ! -L %{_sharedstatedir}/%{name}/config ]; then - mv %{_sharedstatedir}/%{name}/config/* %{_sysconfdir}/%{name}/ - rmdir %{_sharedstatedir}/%{name}/config - ln -sf %{_sysconfdir}/%{name} %{_sharedstatedir}/%{name}/config + if [ ! -L %{_sharedstatedir}/jellyfin/config ]; then + mv %{_sharedstatedir}/jellyfin/config/* %{_sysconfdir}/jellyfin/ + rmdir %{_sharedstatedir}/jellyfin/config + ln -sf %{_sysconfdir}/jellyfin %{_sharedstatedir}/jellyfin/config fi - if [ ! -L %{_sharedstatedir}/%{name}/logs ]; then - mv %{_sharedstatedir}/%{name}/logs/* %{_var}/log/jellyfin - rmdir %{_sharedstatedir}/%{name}/logs - ln -sf %{_var}/log/jellyfin %{_sharedstatedir}/%{name}/logs + if [ ! -L %{_sharedstatedir}/jellyfin/logs ]; then + mv %{_sharedstatedir}/jellyfin/logs/* %{_var}/log/jellyfin + rmdir %{_sharedstatedir}/jellyfin/logs + ln -sf %{_var}/log/jellyfin %{_sharedstatedir}/jellyfin/logs fi - if [ ! -L %{_sharedstatedir}/%{name}/cache ]; then - mv %{_sharedstatedir}/%{name}/cache/* %{_var}/cache/jellyfin - rmdir %{_sharedstatedir}/%{name}/cache - ln -sf %{_var}/cache/jellyfin %{_sharedstatedir}/%{name}/cache + if [ ! -L %{_sharedstatedir}/jellyfin/cache ]; then + mv %{_sharedstatedir}/jellyfin/cache/* %{_var}/cache/jellyfin + rmdir %{_sharedstatedir}/jellyfin/cache + ln -sf %{_var}/cache/jellyfin %{_sharedstatedir}/jellyfin/cache fi if [ "${service_state}" = "active" ]; then systemctl start jellyfin.service @@ -152,13 +132,15 @@ if [ $1 -gt 1 ] ; then fi %systemd_post jellyfin.service -%preun +%preun server %systemd_preun jellyfin.service -%postun +%postun server %systemd_postun_with_restart jellyfin.service %changelog +* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org> +- Forthcoming stable release * Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org> - New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0 * Sat Aug 31 2019 Jellyfin Packaging Team <packaging@jellyfin.org> diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.sudoers b/fedora/jellyfin.sudoers similarity index 100% rename from deployment/fedora-package-x64/pkg-src/jellyfin.sudoers rename to fedora/jellyfin.sudoers diff --git a/deployment/fedora-package-x64/pkg-src/restart.sh b/fedora/restart.sh similarity index 95% rename from deployment/fedora-package-x64/pkg-src/restart.sh rename to fedora/restart.sh index 9b64b6d728..9e53efecd0 100755 --- a/deployment/fedora-package-x64/pkg-src/restart.sh +++ b/fedora/restart.sh @@ -24,13 +24,13 @@ cmd="$( get_service_command )" echo "Detected service control platform '$cmd'; using it to restart Jellyfin..." case $cmd in 'systemctl') - echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now + echo "sleep 2; /usr/bin/sudo $( which systemctl ) restart jellyfin" | at now ;; 'service') - echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now + echo "sleep 2; /usr/bin/sudo $( which service ) jellyfin restart" | at now ;; 'sysv') - echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now + echo "sleep 2; /usr/bin/sudo /etc/init.d/jellyfin restart" | at now ;; esac exit 0 diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index b159db2bd8..9c4b7b0b07 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> @@ -11,7 +16,7 @@ <PackageReference Include="AutoFixture" Version="4.11.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> diff --git a/tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs b/tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs new file mode 100644 index 0000000000..8bf613f05f --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Extensions/StringExtensionsTests.cs @@ -0,0 +1,43 @@ +using System; +using MediaBrowser.Common.Extensions; +using Xunit; + +namespace Jellyfin.Common.Tests.Extensions +{ + public class StringExtensionsTests + { + [Theory] + [InlineData("", 'q', "")] + [InlineData("Banana split", ' ', "Banana")] + [InlineData("Banana split", 'q', "Banana split")] + public void LeftPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult) + { + var result = str.AsSpan().LeftPart(needle).ToString(); + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("", "", "")] + [InlineData("", "q", "")] + [InlineData("Banana split", "", "")] + [InlineData("Banana split", " ", "Banana")] + [InlineData("Banana split test", " split", "Banana")] + public void LeftPart_ValidArgsWithoutStringComparison_Correct(string str, string needle, string expectedResult) + { + var result = str.AsSpan().LeftPart(needle).ToString(); + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("", "", StringComparison.Ordinal, "")] + [InlineData("Banana split", " ", StringComparison.Ordinal, "Banana")] + [InlineData("Banana split test", " split", StringComparison.Ordinal, "Banana")] + [InlineData("Banana split test", " Split", StringComparison.Ordinal, "Banana split test")] + [InlineData("Banana split test", " Splït", StringComparison.InvariantCultureIgnoreCase, "Banana split test")] + public void LeftPart_ValidArgs_Correct(string str, string needle, StringComparison stringComparison, string expectedResult) + { + var result = str.AsSpan().LeftPart(needle, stringComparison).ToString(); + Assert.Equal(expectedResult, result); + } + } +} diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 81a2242e7f..cd41c5604a 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{DF194677-DFD3-42AF-9F75-D44D5A416478}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 30994dee60..407fe2eda1 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 78a020ad58..276c50ca31 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{28464062-0939-4AA7-9F7B-24DDDA61A7C0}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> diff --git a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs new file mode 100644 index 0000000000..51633e157c --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs @@ -0,0 +1,19 @@ +using System; +using MediaBrowser.Model.Extensions; +using Xunit; + +namespace Jellyfin.Model.Tests.Extensions +{ + public class StringHelperTests + { + [Theory] + [InlineData("", "")] + [InlineData("banana", "Banana")] + [InlineData("Banana", "Banana")] + [InlineData("ä", "Ä")] + public void StringHelper_ValidArgs_Success(string input, string expectedResult) + { + Assert.Equal(expectedResult, StringHelper.FirstToUpper(input)); + } + } +} diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj new file mode 100644 index 0000000000..f6c3274986 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.1</TargetFramework> + <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="coverlet.collector" Version="1.2.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" /> + </ItemGroup> + +</Project> diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index f404b3e464..8b14cf800d 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -1,4 +1,9 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> + + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{3998657B-1CCC-49DD-A19F-275DC8495F57}</ProjectGuid> + </PropertyGroup> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> @@ -16,5 +21,15 @@ <ItemGroup> <ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" /> </ItemGroup> + + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> </Project> diff --git a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs index 9a4b0b5422..c9a295a4ce 100644 --- a/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs +++ b/tests/Jellyfin.Naming.Tests/Music/MultiDiscAlbumTests.cs @@ -6,61 +6,45 @@ namespace Jellyfin.Naming.Tests.Music { public class MultiDiscAlbumTests { - [Fact] - public void TestMultiDiscAlbums() + private readonly NamingOptions _namingOptions = new NamingOptions(); + + [Theory] + [InlineData("", false)] + [InlineData("C:/", false)] + [InlineData("/home/", false)] + [InlineData(@"blah blah", false)] + [InlineData(@"D:/music/weezer/03 Pinkerton", false)] + [InlineData(@"D:/music/michael jackson/Bad (2012 Remaster)", false)] + [InlineData(@"cd1", true)] + [InlineData(@"disc18", true)] + [InlineData(@"disk10", true)] + [InlineData(@"vol7", true)] + [InlineData(@"volume1", true)] + [InlineData(@"cd 1", true)] + [InlineData(@"disc 1", true)] + [InlineData(@"disk 1", true)] + [InlineData(@"disk", false)] + [InlineData(@"disk ·", false)] + [InlineData(@"disk a", false)] + [InlineData(@"disk volume", false)] + [InlineData(@"disc disc", false)] + [InlineData(@"disk disc 6", false)] + [InlineData(@"cd - 1", true)] + [InlineData(@"disc- 1", true)] + [InlineData(@"disk - 1", true)] + [InlineData(@"Disc 01 (Hugo Wolf · 24 Lieder)", true)] + [InlineData(@"Disc 04 (Encores and Folk Songs)", true)] + [InlineData(@"Disc04 (Encores and Folk Songs)", true)] + [InlineData(@"Disc 04(Encores and Folk Songs)", true)] + [InlineData(@"Disc04(Encores and Folk Songs)", true)] + [InlineData(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2", true)] + [InlineData(@"[1985] Opportunities (Let's make lots of money) (1985)", false)] + [InlineData(@"Blah 04(Encores and Folk Songs)", false)] + public void AlbumParser_MultidiscPath_Identifies(string path, bool result) { - Assert.False(IsMultiDiscAlbumFolder(@"blah blah")); - Assert.False(IsMultiDiscAlbumFolder(@"D:/music/weezer/03 Pinkerton")); - Assert.False(IsMultiDiscAlbumFolder(@"D:/music/michael jackson/Bad (2012 Remaster)")); + var parser = new AlbumParser(_namingOptions); - Assert.True(IsMultiDiscAlbumFolder(@"cd1")); - Assert.True(IsMultiDiscAlbumFolder(@"disc18")); - Assert.True(IsMultiDiscAlbumFolder(@"disk10")); - Assert.True(IsMultiDiscAlbumFolder(@"vol7")); - Assert.True(IsMultiDiscAlbumFolder(@"volume1")); - - Assert.True(IsMultiDiscAlbumFolder(@"cd 1")); - Assert.True(IsMultiDiscAlbumFolder(@"disc 1")); - Assert.True(IsMultiDiscAlbumFolder(@"disk 1")); - - Assert.False(IsMultiDiscAlbumFolder(@"disk")); - Assert.False(IsMultiDiscAlbumFolder(@"disk ·")); - Assert.False(IsMultiDiscAlbumFolder(@"disk a")); - - Assert.False(IsMultiDiscAlbumFolder(@"disk volume")); - Assert.False(IsMultiDiscAlbumFolder(@"disc disc")); - Assert.False(IsMultiDiscAlbumFolder(@"disk disc 6")); - - Assert.True(IsMultiDiscAlbumFolder(@"cd - 1")); - Assert.True(IsMultiDiscAlbumFolder(@"disc- 1")); - Assert.True(IsMultiDiscAlbumFolder(@"disk - 1")); - - Assert.True(IsMultiDiscAlbumFolder(@"Disc 01 (Hugo Wolf · 24 Lieder)")); - Assert.True(IsMultiDiscAlbumFolder(@"Disc 04 (Encores and Folk Songs)")); - Assert.True(IsMultiDiscAlbumFolder(@"Disc04 (Encores and Folk Songs)")); - Assert.True(IsMultiDiscAlbumFolder(@"Disc 04(Encores and Folk Songs)")); - Assert.True(IsMultiDiscAlbumFolder(@"Disc04(Encores and Folk Songs)")); - - Assert.True(IsMultiDiscAlbumFolder(@"D:/Video/MBTestLibrary/VideoTest/music/.38 special/anth/Disc 2")); - } - - [Fact] - public void TestMultiDiscAlbums1() - { - Assert.False(IsMultiDiscAlbumFolder(@"[1985] Opportunities (Let's make lots of money) (1985)")); - } - - [Fact] - public void TestMultiDiscAlbums2() - { - Assert.False(IsMultiDiscAlbumFolder(@"Blah 04(Encores and Folk Songs)")); - } - - private bool IsMultiDiscAlbumFolder(string path) - { - var parser = new AlbumParser(new NamingOptions()); - - return parser.IsMultiPart(path); + Assert.Equal(result, parser.IsMultiPart(path)); } } } diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs index 41da889c28..d11809de11 100644 --- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs @@ -1,4 +1,5 @@ -using Emby.Naming.Common; +using System; +using Emby.Naming.Common; using Emby.Naming.Subtitles; using Xunit; @@ -6,34 +7,40 @@ namespace Jellyfin.Naming.Tests.Subtitles { public class SubtitleParserTests { - private SubtitleParser GetParser() + private readonly NamingOptions _namingOptions = new NamingOptions(); + + [Theory] + [InlineData("The Skin I Live In (2011).srt", null, false, false)] + [InlineData("The Skin I Live In (2011).eng.srt", "eng", false, false)] + [InlineData("The Skin I Live In (2011).eng.default.srt", "eng", true, false)] + [InlineData("The Skin I Live In (2011).eng.forced.srt", "eng", false, true)] + [InlineData("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true)] + [InlineData("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true)] + [InlineData("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true)] + public void SubtitleParser_ValidFileName_Parses(string input, string language, bool isDefault, bool isForced) { - var options = new NamingOptions(); + var parser = new SubtitleParser(_namingOptions); - return new SubtitleParser(options); - } + var result = parser.ParseFile(input); - [Fact] - public void TestSubtitles() - { - Test("The Skin I Live In (2011).srt", null, false, false); - Test("The Skin I Live In (2011).eng.srt", "eng", false, false); - Test("The Skin I Live In (2011).eng.default.srt", "eng", true, false); - Test("The Skin I Live In (2011).eng.forced.srt", "eng", false, true); - Test("The Skin I Live In (2011).eng.foreign.srt", "eng", false, true); - Test("The Skin I Live In (2011).eng.default.foreign.srt", "eng", true, true); - Test("The Skin I Live In (2011).default.foreign.eng.srt", "eng", true, true); + Assert.Equal(language, result?.Language, true); + Assert.Equal(isDefault, result?.IsDefault); + Assert.Equal(isForced, result?.IsForced); } - private void Test(string input, string language, bool isDefault, bool isForced) + [Theory] + [InlineData("The Skin I Live In (2011).mp4")] + public void SubtitleParser_InvalidFileName_ReturnsNull(string input) { - var parser = GetParser(); + var parser = new SubtitleParser(_namingOptions); - var result = parser.ParseFile(input); + Assert.Null(parser.ParseFile(input)); + } - Assert.Equal(language, result.Language, true); - Assert.Equal(isDefault, result.IsDefault); - Assert.Equal(isForced, result.IsForced); + [Fact] + public void SubtitleParser_EmptyFileName_ThrowsArgumentException() + { + Assert.Throws<ArgumentException>(() => new SubtitleParser(_namingOptions).ParseFile(string.Empty)); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs index 553d06681b..356ba216d6 100644 --- a/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs @@ -21,7 +21,7 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false, null, null, true); - Assert.Equal(episodeNumber, result.EpisodeNumber); + Assert.Equal(episodeNumber, result?.EpisodeNumber); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs index 6ecffe80b7..2937914b9f 100644 --- a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs @@ -6,8 +6,6 @@ namespace Jellyfin.Naming.Tests.TV { public class DailyEpisodeTests { - - [Theory] [InlineData(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)] [InlineData(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)] @@ -23,12 +21,12 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false); - Assert.Null(result.SeasonNumber); - Assert.Null(result.EpisodeNumber); - Assert.Equal(year, result.Year); - Assert.Equal(month, result.Month); - Assert.Equal(day, result.Day); - Assert.Equal(seriesName, result.SeriesName, true); + Assert.Null(result?.SeasonNumber); + Assert.Null(result?.EpisodeNumber); + Assert.Equal(year, result?.Year); + Assert.Equal(month, result?.Month); + Assert.Equal(day, result?.Day); + Assert.Equal(seriesName, result?.SeriesName, true); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs index 0c7d9520e2..8bd1a43d62 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs @@ -6,7 +6,6 @@ namespace Jellyfin.Naming.Tests.TV { public class EpisodeNumberWithoutSeasonTests { - [Theory] [InlineData(8, @"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")] [InlineData(2, @"The Simpsons/The Simpsons - 02 - Ep Name.avi")] @@ -30,7 +29,7 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false); - Assert.Equal(episodeNumber, result.EpisodeNumber); + Assert.Equal(episodeNumber, result?.EpisodeNumber); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs index 4b56067153..03aeb7f76b 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs @@ -1,4 +1,4 @@ -using Emby.Naming.Common; +using Emby.Naming.Common; using Emby.Naming.TV; using Xunit; @@ -35,7 +35,6 @@ namespace Jellyfin.Naming.Tests.TV // TODO: [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", "Watchmen (2019)", 1, 3)] // TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", "The Legend of Condor Heroes 2017", 1, 7)] public void ParseEpisodesCorrectly(string path, string name, int season, int episode) - { NamingOptions o = new NamingOptions(); EpisodePathParser p = new EpisodePathParser(o); diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs index 364eb7ff85..d0418a49ed 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs @@ -19,9 +19,9 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false); - Assert.Equal(seasonNumber, result.SeasonNumber); - Assert.Equal(episodeNumber, result.EpisodeNumber); - Assert.Equal(seriesName, result.SeriesName, true); + Assert.Equal(seasonNumber, result?.SeasonNumber); + Assert.Equal(episodeNumber, result?.EpisodeNumber); + Assert.Equal(seriesName, result?.SeriesName, ignoreCase: true); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs index 9eaf897b9e..4837e3a3b4 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(_namingOptions) .Resolve(path, false); - Assert.Equal(expected, result.SeasonNumber); + Assert.Equal(expected, result?.SeasonNumber); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs index de253ce375..40b41b9f3d 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs @@ -31,9 +31,9 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false); - Assert.Equal(seasonNumber, result.SeasonNumber); - Assert.Equal(episodeNumber, result.EpisodeNumber); - Assert.Equal(seriesName, result.SeriesName, true); + Assert.Equal(seasonNumber, result?.SeasonNumber); + Assert.Equal(episodeNumber, result?.EpisodeNumber); + Assert.Equal(seriesName, result?.SeriesName, true); } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs b/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs deleted file mode 100644 index 0c2978aca9..0000000000 --- a/tests/Jellyfin.Naming.Tests/Video/BaseVideoTest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Emby.Naming.Common; -using Emby.Naming.Video; - -namespace Jellyfin.Naming.Tests.Video -{ - public abstract class BaseVideoTest - { - private readonly NamingOptions _namingOptions = new NamingOptions(); - - protected VideoResolver GetParser() - => new VideoResolver(_namingOptions); - } -} diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index 49cb2387bb..917d8fb3a9 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -46,6 +46,7 @@ namespace Jellyfin.Naming.Tests.Video [InlineData("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016)] // FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)] [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again + [InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)] public void CleanDateTimeTest(string input, string expectedName, int? expectedYear) { input = Path.GetFileName(input); diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index a64d173496..a2722a1753 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -5,7 +5,7 @@ using Xunit; namespace Jellyfin.Naming.Tests.Video { - public class ExtraTests : BaseVideoTest + public class ExtraTests { private readonly NamingOptions _videoOptions = new NamingOptions(); diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs index ed3112936d..69de96a47a 100644 --- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs @@ -4,29 +4,29 @@ using Xunit; namespace Jellyfin.Naming.Tests.Video { - public class Format3DTests : BaseVideoTest + public class Format3DTests { + private readonly NamingOptions _namingOptions = new NamingOptions(); + [Fact] public void TestKodiFormat3D() { - var options = new NamingOptions(); - - Test("Super movie.3d.mp4", false, null, options); - Test("Super movie.3d.hsbs.mp4", true, "hsbs", options); - Test("Super movie.3d.sbs.mp4", true, "sbs", options); - Test("Super movie.3d.htab.mp4", true, "htab", options); - Test("Super movie.3d.tab.mp4", true, "tab", options); - Test("Super movie 3d hsbs.mp4", true, "hsbs", options); + Test("Super movie.3d.mp4", false, null); + Test("Super movie.3d.hsbs.mp4", true, "hsbs"); + Test("Super movie.3d.sbs.mp4", true, "sbs"); + Test("Super movie.3d.htab.mp4", true, "htab"); + Test("Super movie.3d.tab.mp4", true, "tab"); + Test("Super movie 3d hsbs.mp4", true, "hsbs"); } [Fact] public void Test3DName() { var result = - GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv"); + new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv"); - Assert.Equal("hsbs", result.Format3D); - Assert.Equal("Oblivion", result.Name); + Assert.Equal("hsbs", result?.Format3D); + Assert.Equal("Oblivion", result?.Name); } [Fact] @@ -34,32 +34,31 @@ namespace Jellyfin.Naming.Tests.Video { // These were introduced for Media Browser 3 // Kodi conventions are preferred but these still need to be supported - var options = new NamingOptions(); - Test("Super movie.3d.mp4", false, null, options); - Test("Super movie.3d.hsbs.mp4", true, "hsbs", options); - Test("Super movie.3d.sbs.mp4", true, "sbs", options); - Test("Super movie.3d.htab.mp4", true, "htab", options); - Test("Super movie.3d.tab.mp4", true, "tab", options); + Test("Super movie.3d.mp4", false, null); + Test("Super movie.3d.hsbs.mp4", true, "hsbs"); + Test("Super movie.3d.sbs.mp4", true, "sbs"); + Test("Super movie.3d.htab.mp4", true, "htab"); + Test("Super movie.3d.tab.mp4", true, "tab"); - Test("Super movie.hsbs.mp4", true, "hsbs", options); - Test("Super movie.sbs.mp4", true, "sbs", options); - Test("Super movie.htab.mp4", true, "htab", options); - Test("Super movie.tab.mp4", true, "tab", options); - Test("Super movie.sbs3d.mp4", true, "sbs3d", options); - Test("Super movie.3d.mvc.mp4", true, "mvc", options); + Test("Super movie.hsbs.mp4", true, "hsbs"); + Test("Super movie.sbs.mp4", true, "sbs"); + Test("Super movie.htab.mp4", true, "htab"); + Test("Super movie.tab.mp4", true, "tab"); + Test("Super movie.sbs3d.mp4", true, "sbs3d"); + Test("Super movie.3d.mvc.mp4", true, "mvc"); - Test("Super movie [3d].mp4", false, null, options); - Test("Super movie [hsbs].mp4", true, "hsbs", options); - Test("Super movie [fsbs].mp4", true, "fsbs", options); - Test("Super movie [ftab].mp4", true, "ftab", options); - Test("Super movie [htab].mp4", true, "htab", options); - Test("Super movie [sbs3d].mp4", true, "sbs3d", options); + Test("Super movie [3d].mp4", false, null); + Test("Super movie [hsbs].mp4", true, "hsbs"); + Test("Super movie [fsbs].mp4", true, "fsbs"); + Test("Super movie [ftab].mp4", true, "ftab"); + Test("Super movie [htab].mp4", true, "htab"); + Test("Super movie [sbs3d].mp4", true, "sbs3d"); } - private void Test(string input, bool is3D, string format3D, NamingOptions options) + private void Test(string input, bool is3D, string? format3D) { - var parser = new Format3DParser(options); + var parser = new Format3DParser(_namingOptions); var result = parser.Parse(input); diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index b8fbb2cb25..4198d69ff8 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -8,9 +8,11 @@ namespace Jellyfin.Naming.Tests.Video { public class MultiVersionTests { + private readonly NamingOptions _namingOptions = new NamingOptions(); + // FIXME // [Fact] - public void TestMultiEdition1() + private void TestMultiEdition1() { var files = new[] { @@ -26,7 +28,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -35,7 +36,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiEdition2() + private void TestMultiEdition2() { var files = new[] { @@ -51,7 +52,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -74,7 +74,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -83,7 +82,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestLetterFolders() + private void TestLetterFolders() { var files = new[] { @@ -102,7 +101,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(7, result.Count); @@ -112,7 +110,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersionLimit() + private void TestMultiVersionLimit() { var files = new[] { @@ -132,7 +130,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -142,7 +139,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersionLimit2() + private void TestMultiVersionLimit2() { var files = new[] { @@ -163,7 +160,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(9, result.Count); @@ -173,7 +169,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion3() + private void TestMultiVersion3() { var files = new[] { @@ -190,7 +186,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -200,7 +195,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion4() + private void TestMultiVersion4() { // Test for false positive @@ -219,7 +214,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -229,7 +223,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion5() + private void TestMultiVersion5() { var files = new[] { @@ -249,7 +243,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -262,7 +255,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion6() + private void TestMultiVersion6() { var files = new[] { @@ -282,7 +275,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -295,7 +287,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion7() + private void TestMultiVersion7() { var files = new[] { @@ -309,7 +301,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(2, result.Count); @@ -317,7 +308,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion8() + private void TestMultiVersion8() { // This is not actually supported yet @@ -338,7 +329,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -351,7 +341,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion9() + private void TestMultiVersion9() { // Test for false positive @@ -370,7 +360,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -380,7 +369,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion10() + private void TestMultiVersion10() { var files = new[] { @@ -394,7 +383,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -404,7 +392,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion11() + private void TestMultiVersion11() { // Currently not supported but we should probably handle this. @@ -420,7 +408,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -430,8 +417,7 @@ namespace Jellyfin.Naming.Tests.Video private VideoListResolver GetResolver() { - var options = new NamingOptions(); - return new VideoListResolver(options); + return new VideoListResolver(_namingOptions); } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs index 3e0cbaf0c2..8794d3ebe8 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs @@ -6,8 +6,10 @@ using Xunit; namespace Jellyfin.Naming.Tests.Video { - public class StackTests : BaseVideoTest + public class StackTests { + private readonly NamingOptions _namingOptions = new NamingOptions(); + [Fact] public void TestSimpleStack() { @@ -366,11 +368,11 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - new FileSystemMetadata{FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false}, - new FileSystemMetadata{FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false}, - new FileSystemMetadata{FullName = "300 (2006) part2", IsDirectory = true}, - new FileSystemMetadata{FullName = "300 (2006) part3", IsDirectory = true}, - new FileSystemMetadata{FullName = "300 (2006) part1", IsDirectory = true} + new FileSystemMetadata { FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false }, + new FileSystemMetadata { FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false }, + new FileSystemMetadata { FullName = "300 (2006) part2", IsDirectory = true }, + new FileSystemMetadata { FullName = "300 (2006) part3", IsDirectory = true }, + new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true } }; var resolver = GetResolver(); @@ -446,7 +448,7 @@ namespace Jellyfin.Naming.Tests.Video private StackResolver GetResolver() { - return new StackResolver(new NamingOptions()); + return new StackResolver(_namingOptions); } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs index 8d5ced9a41..30ba941365 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs @@ -4,8 +4,10 @@ using Xunit; namespace Jellyfin.Naming.Tests.Video { - public class StubTests : BaseVideoTest + public class StubTests { + private readonly NamingOptions _namingOptions = new NamingOptions(); + [Fact] public void TestStubs() { @@ -27,16 +29,14 @@ namespace Jellyfin.Naming.Tests.Video public void TestStubName() { var result = - GetParser().ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc"); + new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc"); - Assert.Equal("Oblivion", result.Name); + Assert.Equal("Oblivion", result?.Name); } - private void Test(string path, bool isStub, string stubType) + private void Test(string path, bool isStub, string? stubType) { - var options = new NamingOptions(); - - var isStubResult = StubResolver.TryResolveFile(path, options, out var stubTypeResult); + var isStubResult = StubResolver.TryResolveFile(path, _namingOptions, out var stubTypeResult); Assert.Equal(isStub, isStubResult); diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index ef8a178989..12c4a50fe3 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -8,9 +8,11 @@ namespace Jellyfin.Naming.Tests.Video { public class VideoListResolverTests { + private readonly NamingOptions _namingOptions = new NamingOptions(); + // FIXME // [Fact] - public void TestStackAndExtras() + private void TestStackAndExtras() { // No stacking here because there is no part/disc/etc var files = new[] @@ -44,7 +46,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -73,7 +74,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -94,7 +94,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -115,7 +114,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -137,7 +135,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -158,7 +155,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -183,7 +179,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -204,7 +199,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = true, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -226,7 +220,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = true, FullName = i - }).ToList()).ToList(); Assert.Equal(2, result.Count); @@ -248,7 +241,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -270,7 +262,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -293,7 +284,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -316,7 +306,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(2, result.Count); @@ -336,7 +325,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -356,7 +344,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -377,7 +364,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -398,7 +384,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -421,7 +406,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(4, result.Count); @@ -442,7 +426,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -450,8 +433,7 @@ namespace Jellyfin.Naming.Tests.Video private VideoListResolver GetResolver() { - var options = new NamingOptions(); - return new VideoListResolver(options); + return new VideoListResolver(_namingOptions); } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index 5a3ce88869..99828b2eb7 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -1,275 +1,199 @@ -using MediaBrowser.Model.Entities; +using System.Collections.Generic; +using Emby.Naming.Common; +using Emby.Naming.Video; +using MediaBrowser.Model.Entities; using Xunit; namespace Jellyfin.Naming.Tests.Video { - public class VideoResolverTests : BaseVideoTest + public class VideoResolverTests { - // FIXME - // [Fact] - public void TestSimpleFile() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).mkv"); - - Assert.Equal(2006, result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Equal("Brave", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void TestSimpleFile2() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv"); - - Assert.Equal(1995, result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Equal("Bad Boys", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void TestSimpleFileWithNumericName() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).mkv"); - - Assert.Equal(2006, result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Equal("300", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void TestExtra() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv"); - - Assert.Equal(2006, result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Equal(ExtraType.Trailer, result.ExtraType); - Assert.Equal("Brave (2006)-trailer", result.Name); - } - - // FIXME - // [Fact] - public void TestExtraWithNumericName() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.mkv"); - - Assert.Equal(2006, result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Equal("300 (2006)-trailer", result.Name); - Assert.Equal(ExtraType.Trailer, result.ExtraType); - } - - // FIXME - // [Fact] - public void TestStubFileWithNumericName() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).bluray.disc"); - - Assert.Equal(2006, result.Year); - Assert.True(result.IsStub); - Assert.Equal("bluray", result.StubType); - Assert.False(result.Is3D); - Assert.Equal("300", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void TestStubFile() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/Brave (2007)/Brave (2006).bluray.disc"); - - Assert.Equal(2006, result.Year); - Assert.True(result.IsStub); - Assert.Equal("bluray", result.StubType); - Assert.False(result.Is3D); - Assert.Equal("Brave", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void TestExtraStubWithNumericNameNotSupported() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc"); - - Assert.Equal(2006, result.Year); - Assert.True(result.IsStub); - Assert.Equal("bluray", result.StubType); - Assert.False(result.Is3D); - Assert.Equal("300", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void TestExtraStubNotSupported() - { - // Using a stub for an extra is currently not supported - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc"); - - Assert.Equal(2006, result.Year); - Assert.True(result.IsStub); - Assert.Equal("bluray", result.StubType); - Assert.False(result.Is3D); - Assert.Equal("brave", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void Test3DFileWithNumericName() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv"); - - Assert.Equal(2006, result.Year); - Assert.False(result.IsStub); - Assert.True(result.Is3D); - Assert.Equal("sbs", result.Format3D); - Assert.Equal("300", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void TestBad3DFileWithNumericName() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv"); - - Assert.Equal(2006, result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Equal("300", result.Name); - Assert.Null(result.ExtraType); - Assert.Null(result.Format3D); - } - - // FIXME - // [Fact] - public void Test3DFile() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv"); - - Assert.Equal(2006, result.Year); - Assert.False(result.IsStub); - Assert.True(result.Is3D); - Assert.Equal("sbs", result.Format3D); - Assert.Equal("brave", result.Name); - Assert.Null(result.ExtraType); - } - - [Fact] - public void TestNameWithoutDate() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/American Psycho/American.Psycho.mkv"); - - Assert.Null(result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Null(result.Format3D); - Assert.Equal("American.Psycho", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void TestCleanDateAndStringsSequence() - { - var parser = GetParser(); - - // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again - var result = - parser.ResolveFile(@"/server/Movies/3.Days.to.Kill/3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv"); - - Assert.Equal(2014, result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Null(result.Format3D); - Assert.Equal("3.Days.to.Kill", result.Name); - Assert.Null(result.ExtraType); - } - - // FIXME - // [Fact] - public void TestCleanDateAndStringsSequence1() - { - var parser = GetParser(); - - // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again - var result = - parser.ResolveFile(@"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv"); - - Assert.Equal(2005, result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Null(result.Format3D); - Assert.Equal("3 days to kill", result.Name); - Assert.Null(result.ExtraType); - } - - [Fact] - public void TestFolderNameWithExtension() - { - var parser = GetParser(); - - var result = - parser.ResolveFile(@"/server/Movies/7 Psychos.mkv/7 Psychos.mkv"); - - Assert.Null(result.Year); - Assert.False(result.IsStub); - Assert.False(result.Is3D); - Assert.Equal("7 Psychos", result.Name); - Assert.Null(result.ExtraType); + private readonly NamingOptions _namingOptions = new NamingOptions(); + + public static IEnumerable<object[]> GetResolveFileTestData() + { + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv", + Container = "mkv", + Name = "7 Psychos" + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv", + Container = "mkv", + Name = "3 days to kill", + Year = 2005 + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/American Psycho/American.Psycho.mkv", + Container = "mkv", + Name = "American.Psycho", + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv", + Container = "mkv", + Name = "brave", + Year = 2006, + Is3D = true, + Format3D = "sbs", + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv", + Container = "mkv", + Name = "300", + Year = 2006 + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv", + Container = "mkv", + Name = "300", + Year = 2006, + Is3D = true, + Format3D = "sbs", + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc", + Container = "disc", + Name = "brave", + Year = 2006, + IsStub = true, + StubType = "bluray", + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc", + Container = "disc", + Name = "300", + Year = 2006, + IsStub = true, + StubType = "bluray", + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc", + Container = "disc", + Name = "Brave", + Year = 2006, + IsStub = true, + StubType = "bluray", + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/300 (2007)/300 (2006).bluray.disc", + Container = "disc", + Name = "300", + Year = 2006, + IsStub = true, + StubType = "bluray", + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv", + Container = "mkv", + Name = "300", + Year = 2006, + ExtraType = ExtraType.Trailer, + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv", + Container = "mkv", + Name = "Brave", + Year = 2006, + ExtraType = ExtraType.Trailer, + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/300 (2007)/300 (2006).mkv", + Container = "mkv", + Name = "300", + Year = 2006 + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv", + Container = "mkv", + Name = "Bad Boys", + Year = 1995, + } + }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/Brave (2007)/Brave (2006).mkv", + Container = "mkv", + Name = "Brave", + Year = 2006, + } + }; + } + + [Theory] + [MemberData(nameof(GetResolveFileTestData))] + public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult) + { + var result = new VideoResolver(_namingOptions).ResolveFile(expectedResult.Path); + + Assert.NotNull(result); + Assert.Equal(result?.Path, expectedResult.Path); + Assert.Equal(result?.Container, expectedResult.Container); + Assert.Equal(result?.Name, expectedResult.Name); + Assert.Equal(result?.Year, expectedResult.Year); + Assert.Equal(result?.ExtraType, expectedResult.ExtraType); + Assert.Equal(result?.Format3D, expectedResult.Format3D); + Assert.Equal(result?.Is3D, expectedResult.Is3D); + Assert.Equal(result?.IsStub, expectedResult.IsStub); + Assert.Equal(result?.StubType, expectedResult.StubType); + Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory); + Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension); } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs new file mode 100644 index 0000000000..39bd94b598 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs @@ -0,0 +1,18 @@ +using Emby.Server.Implementations.HttpServer; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.HttpServer +{ + public class ResponseFilterTests + { + [Theory] + [InlineData(null, null)] + [InlineData("", "")] + [InlineData("This is a clean string.", "This is a clean string.")] + [InlineData("This isn't \n\ra clean string.", "This isn't a clean string.")] + public void RemoveControlCharacters_ValidArgs_Correct(string? input, string? result) + { + Assert.Equal(result, ResponseFilter.RemoveControlCharacters(input)); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index b7865439c7..ba7ecb3d13 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -1,5 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> + <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> + <PropertyGroup> + <ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid> + </PropertyGroup> + <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs new file mode 100644 index 0000000000..26dee38c6d --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -0,0 +1,21 @@ +using Emby.Server.Implementations.Library; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library +{ + public class IgnorePatternsTests + { + [Theory] + [InlineData("/media/small.jpg", true)] + [InlineData("/media/movies/#Recycle/test.txt", true)] + [InlineData("/media/movies/#recycle/", true)] + [InlineData("thumbs.db", true)] + [InlineData(@"C:\media\movies\movie.avi", false)] + [InlineData("/media/.hiddendir/file.mp4", true)] + [InlineData("/media/dir/.hiddenfile.mp4", true)] + public void PathIgnored(string path, bool expected) + { + Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs new file mode 100644 index 0000000000..c771f5f4ae --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -0,0 +1,27 @@ +using System; +using Emby.Server.Implementations.Library; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library +{ + public class PathExtensionsTests + { + [Theory] + [InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son", "imdbid", null)] + public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult) + { + Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute)); + } + + [Theory] + [InlineData("", "")] + [InlineData("Superman: Red Son [imdbid=tt10985510]", "")] + [InlineData("", "imdbid")] + public void GetAttributeValue_EmptyString_ThrowsArgumentException(string input, string attribute) + { + Assert.Throws<ArgumentException>(() => PathExtensions.GetAttributeValue(input, attribute)); + } + } +} diff --git a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs new file mode 100644 index 0000000000..34698fe251 --- /dev/null +++ b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs @@ -0,0 +1,49 @@ +using System.Text.Json; +using System.Threading.Tasks; +using MediaBrowser.Model.Branding; +using Xunit; + +namespace MediaBrowser.Api.Tests +{ + public sealed class BrandingServiceTests : IClassFixture<JellyfinApplicationFactory> + { + private readonly JellyfinApplicationFactory _factory; + + public BrandingServiceTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetConfiguration_ReturnsCorrectResponse() + { + // Arrange + var client = _factory.CreateClient(); + + // Act + var response = await client.GetAsync("/Branding/Configuration"); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString()); + var responseBody = await response.Content.ReadAsStreamAsync(); + _ = await JsonSerializer.DeserializeAsync<BrandingOptions>(responseBody); + } + + [Theory] + [InlineData("/Branding/Css")] + [InlineData("/Branding/Css.css")] + public async Task GetCss_ReturnsCorrectResponse(string url) + { + // Arrange + var client = _factory.CreateClient(); + + // Act + var response = await client.GetAsync(url); + + // Assert + response.EnsureSuccessStatusCode(); + Assert.Equal("text/css", response.Content.Headers.ContentType.ToString()); + } + } +} diff --git a/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs new file mode 100644 index 0000000000..c39ed07de3 --- /dev/null +++ b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using Emby.Server.Implementations; +using Emby.Server.Implementations.IO; +using Emby.Server.Implementations.Networking; +using Jellyfin.Drawing.Skia; +using Jellyfin.Server; +using MediaBrowser.Common; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Extensions.Logging; + +namespace MediaBrowser.Api.Tests +{ + /// <summary> + /// Factory for bootstrapping the Jellyfin application in memory for functional end to end tests. + /// </summary> + public class JellyfinApplicationFactory : WebApplicationFactory<Startup> + { + private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data"); + private static readonly ConcurrentBag<IDisposable> _disposableComponents = new ConcurrentBag<IDisposable>(); + + /// <summary> + /// Initializes a new instance of the <see cref="JellyfinApplicationFactory"/> class. + /// </summary> + public JellyfinApplicationFactory() + { + // Perform static initialization that only needs to happen once per test-run + Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); + Program.PerformStaticInitialization(); + } + + /// <inheritdoc/> + protected override IWebHostBuilder CreateWebHostBuilder() + { + return new WebHostBuilder(); + } + + /// <inheritdoc/> + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + // Specify the startup command line options + var commandLineOpts = new StartupOptions + { + NoWebClient = true, + NoAutoRunWebApp = true + }; + + // Use a temporary directory for the application paths + var webHostPathRoot = Path.Combine(_testPathRoot, "test-host-" + Path.GetFileNameWithoutExtension(Path.GetRandomFileName())); + Directory.CreateDirectory(Path.Combine(webHostPathRoot, "logs")); + Directory.CreateDirectory(Path.Combine(webHostPathRoot, "config")); + Directory.CreateDirectory(Path.Combine(webHostPathRoot, "cache")); + Directory.CreateDirectory(Path.Combine(webHostPathRoot, "jellyfin-web")); + var appPaths = new ServerApplicationPaths( + webHostPathRoot, + Path.Combine(webHostPathRoot, "logs"), + Path.Combine(webHostPathRoot, "config"), + Path.Combine(webHostPathRoot, "cache"), + Path.Combine(webHostPathRoot, "jellyfin-web")); + + // Create the logging config file + // TODO: We shouldn't need to do this since we are only logging to console + Program.InitLoggingConfigFile(appPaths).GetAwaiter().GetResult(); + + // Create a copy of the application configuration to use for startup + var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths); + + ILoggerFactory loggerFactory = new SerilogLoggerFactory(); + _disposableComponents.Add(loggerFactory); + + // Create the app host and initialize it + var appHost = new CoreAppHost( + appPaths, + loggerFactory, + commandLineOpts, + new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), + new NetworkManager(loggerFactory.CreateLogger<NetworkManager>())); + _disposableComponents.Add(appHost); + var serviceCollection = new ServiceCollection(); + appHost.Init(serviceCollection); + + // Configure the web host builder + Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths); + } + + /// <inheritdoc/> + protected override TestServer CreateServer(IWebHostBuilder builder) + { + // Create the test server using the base implementation + var testServer = base.CreateServer(builder); + + // Finish initializing the app host + var appHost = (CoreAppHost)testServer.Services.GetRequiredService<IApplicationHost>(); + appHost.ServiceProvider = testServer.Services; + appHost.InitializeServices().GetAwaiter().GetResult(); + appHost.RunStartupTasksAsync().GetAwaiter().GetResult(); + + return testServer; + } + + /// <inheritdoc/> + protected override void Dispose(bool disposing) + { + foreach (var disposable in _disposableComponents) + { + disposable.Dispose(); + } + + _disposableComponents.Clear(); + + base.Dispose(disposing); + } + } +} diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj new file mode 100644 index 0000000000..60c392314d --- /dev/null +++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj @@ -0,0 +1,33 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.1</TargetFramework> + <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="coverlet.collector" Version="1.2.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" /> + <ProjectReference Include="..\..\MediaBrowser.Api\MediaBrowser.Api.csproj" /> + </ItemGroup> + + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + +</Project> diff --git a/tests/jellyfin-tests.ruleset b/tests/jellyfin-tests.ruleset new file mode 100644 index 0000000000..5a113e955e --- /dev/null +++ b/tests/jellyfin-tests.ruleset @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<RuleSet Name="Rules for MediaBrowser.Api.Tests" Description="Code analysis rules for MediaBrowser.Api.Tests.csproj" ToolsVersion="14.0"> + + <!-- Include the solution default RuleSet. The rules in this file will override the defaults. --> + <Include Path="../jellyfin.ruleset" Action="Default" /> + + <!-- StyleCop Analyzer Rules --> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers"> + <!-- SA0001: XML comment analysis is disabled due to project configuration --> + <Rule Id="SA0001" Action="None" /> + </Rules> + + <!-- FxCop Analyzer Rules --> + <Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design"> + <!-- CA1707: Identifiers should not contain underscores --> + <Rule Id="CA1707" Action="None" /> + <!-- CA2007: Consider calling ConfigureAwait on the awaited task --> + <Rule Id="CA2007" Action="None" /> + <!-- CA2234: Pass system uri objects instead of strings --> + <Rule Id="CA2234" Action="Info" /> + </Rules> +</RuleSet> diff --git a/windows/build-jellyfin.ps1 b/windows/build-jellyfin.ps1 new file mode 100644 index 0000000000..c762137a75 --- /dev/null +++ b/windows/build-jellyfin.ps1 @@ -0,0 +1,190 @@ +[CmdletBinding()] +param( + [switch]$MakeNSIS, + [switch]$InstallNSIS, + [switch]$InstallFFMPEG, + [switch]$InstallNSSM, + [switch]$SkipJellyfinBuild, + [switch]$GenerateZip, + [string]$InstallLocation = "./dist/jellyfin-win-nsis", + [string]$UXLocation = "../jellyfin-ux", + [switch]$InstallTrayApp, + [ValidateSet('Debug','Release')][string]$BuildType = 'Release', + [ValidateSet('Quiet','Minimal', 'Normal')][string]$DotNetVerbosity = 'Minimal', + [ValidateSet('win','win7', 'win8','win81','win10')][string]$WindowsVersion = 'win', + [ValidateSet('x64','x86', 'arm', 'arm64')][string]$Architecture = 'x64' +) + +$ProgressPreference = 'SilentlyContinue' # Speedup all downloads by hiding progress bars. + +#PowershellCore and *nix check to make determine which temp dir to use. +if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){ + $TempDir = mktemp -d +}else{ + $TempDir = $env:Temp +} +#Create staging dir +New-Item -ItemType Directory -Force -Path $InstallLocation +$ResolvedInstallLocation = Resolve-Path $InstallLocation +$ResolvedUXLocation = Resolve-Path $UXLocation + +function Build-JellyFin { + if(($Architecture -eq 'arm64') -and ($WindowsVersion -ne 'win10')){ + Write-Error "arm64 only supported with Windows10 Version" + exit + } + if(($Architecture -eq 'arm') -and ($WindowsVersion -notin @('win10','win81','win8'))){ + Write-Error "arm only supported with Windows 8 or higher" + exit + } + Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture" + Write-Verbose "InstallLocation: $ResolvedInstallLocation" + Write-Verbose "DotNetVerbosity: $DotNetVerbosity" + dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server +} + +function Install-FFMPEG { + param( + [string]$ResolvedInstallLocation, + [string]$Architecture, + [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared" + ) + Write-Verbose "Checking Architecture" + if($Architecture -notin @('x86','x64')){ + Write-Warning "No builds available for your selected architecture of $Architecture" + Write-Warning "FFMPEG will not be installed" + }elseif($Architecture -eq 'x64'){ + Write-Verbose "Downloading 64 bit FFMPEG" + Invoke-WebRequest -Uri https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose + }else{ + Write-Verbose "Downloading 32 bit FFMPEG" + Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/$FFMPEGVersionX86.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose + } + + Expand-Archive "$tempdir/ffmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" -Force | Write-Verbose + if($Architecture -eq 'x64'){ + Write-Verbose "Copying Binaries to Jellyfin location" + Get-ChildItem "$tempdir/ffmpeg" | ForEach-Object { + Copy-Item $_.FullName -Destination $installLocation | Write-Verbose + } + }else{ + Write-Verbose "Copying Binaries to Jellyfin location" + Get-ChildItem "$tempdir/ffmpeg/$FFMPEGVersionX86/bin" | ForEach-Object { + Copy-Item $_.FullName -Destination $installLocation | Write-Verbose + } + } + Remove-Item "$tempdir/ffmpeg/" -Recurse -Force -ErrorAction Continue | Write-Verbose + Remove-Item "$tempdir/ffmpeg.zip" -Force -ErrorAction Continue | Write-Verbose +} + +function Install-NSSM { + param( + [string]$ResolvedInstallLocation, + [string]$Architecture + ) + Write-Verbose "Checking Architecture" + if($Architecture -notin @('x86','x64')){ + Write-Warning "No builds available for your selected architecture of $Architecture" + Write-Warning "NSSM will not be installed" + }else{ + Write-Verbose "Downloading NSSM" + # [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + # Temporary workaround, file is hosted in an azure blob with a custom domain in front for brevity + Invoke-WebRequest -Uri http://files.evilt.win/nssm/nssm-2.24-101-g897c7ad.zip -UseBasicParsing -OutFile "$tempdir/nssm.zip" | Write-Verbose + } + + Expand-Archive "$tempdir/nssm.zip" -DestinationPath "$tempdir/nssm/" -Force | Write-Verbose + if($Architecture -eq 'x64'){ + Write-Verbose "Copying Binaries to Jellyfin location" + Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win64" | ForEach-Object { + Copy-Item $_.FullName -Destination $installLocation | Write-Verbose + } + }else{ + Write-Verbose "Copying Binaries to Jellyfin location" + Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win32" | ForEach-Object { + Copy-Item $_.FullName -Destination $installLocation | Write-Verbose + } + } + Remove-Item "$tempdir/nssm/" -Recurse -Force -ErrorAction Continue | Write-Verbose + Remove-Item "$tempdir/nssm.zip" -Force -ErrorAction Continue | Write-Verbose +} + +function Make-NSIS { + param( + [string]$ResolvedInstallLocation + ) + + $env:InstallLocation = $ResolvedInstallLocation + if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){ + & "$tempdir/nsis/nsis-3.04/makensis.exe" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi" + } else { + & "makensis" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi" + } + Copy-Item .\deployment\windows\jellyfin_*.exe $ResolvedInstallLocation\..\ +} + + +function Install-NSIS { + Write-Verbose "Downloading NSIS" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri https://nchc.dl.sourceforge.net/project/nsis/NSIS%203/3.04/nsis-3.04.zip -UseBasicParsing -OutFile "$tempdir/nsis.zip" | Write-Verbose + + Expand-Archive "$tempdir/nsis.zip" -DestinationPath "$tempdir/nsis/" -Force | Write-Verbose +} + +function Cleanup-NSIS { + Remove-Item "$tempdir/nsis/" -Recurse -Force -ErrorAction Continue | Write-Verbose + Remove-Item "$tempdir/nsis.zip" -Force -ErrorAction Continue | Write-Verbose +} + +function Install-TrayApp { + param( + [string]$ResolvedInstallLocation, + [string]$Architecture + ) + Write-Verbose "Checking Architecture" + if($Architecture -ne 'x64'){ + Write-Warning "No builds available for your selected architecture of $Architecture" + Write-Warning "The tray app will not be available." + }else{ + Write-Verbose "Downloading Tray App and copying to Jellyfin location" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri https://github.com/jellyfin/jellyfin-windows-tray/releases/latest/download/JellyfinTray.exe -UseBasicParsing -OutFile "$installLocation/JellyfinTray.exe" | Write-Verbose + } +} + +if(-not $SkipJellyfinBuild.IsPresent -and -not ($InstallNSIS -eq $true)){ + Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture" + Build-JellyFin +} +if($InstallFFMPEG.IsPresent -or ($InstallFFMPEG -eq $true)){ + Write-Verbose "Starting FFMPEG Install" + Install-FFMPEG $ResolvedInstallLocation $Architecture +} +if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){ + Write-Verbose "Starting NSSM Install" + Install-NSSM $ResolvedInstallLocation $Architecture +} +if($InstallTrayApp.IsPresent -or ($InstallTrayApp -eq $true)){ + Write-Verbose "Downloading Windows Tray App" + Install-TrayApp $ResolvedInstallLocation $Architecture +} +#Copy-Item .\deployment\windows\install-jellyfin.ps1 $ResolvedInstallLocation\install-jellyfin.ps1 +#Copy-Item .\deployment\windows\install.bat $ResolvedInstallLocation\install.bat +Copy-Item .\LICENSE $ResolvedInstallLocation\LICENSE +if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){ + Write-Verbose "Installing NSIS" + Install-NSIS +} +if($MakeNSIS.IsPresent -or ($MakeNSIS -eq $true)){ + Write-Verbose "Starting NSIS Package creation" + Make-NSIS $ResolvedInstallLocation +} +if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){ + Write-Verbose "Cleanup NSIS" + Cleanup-NSIS +} +if($GenerateZip.IsPresent -or ($GenerateZip -eq $true)){ + Compress-Archive -Path $ResolvedInstallLocation -DestinationPath "$ResolvedInstallLocation/jellyfin.zip" -Force +} +Write-Verbose "Finished" diff --git a/windows/dependencies.txt b/windows/dependencies.txt new file mode 100644 index 0000000000..16f77cce7c --- /dev/null +++ b/windows/dependencies.txt @@ -0,0 +1,2 @@ +dotnet +nsis diff --git a/windows/dialogs/confirmation.nsddef b/windows/dialogs/confirmation.nsddef new file mode 100644 index 0000000000..969ebacd62 --- /dev/null +++ b/windows/dialogs/confirmation.nsddef @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +This file was created by NSISDialogDesigner 1.4.4.0 +http://coolsoft.altervista.org/nsisdialogdesigner +Do not edit manually! +--> +<Dialog Name="confirmation" Title="Confirmation Page" Subtitle="Please confirm your choices for Jellyfin Server installation" GenerateShowFunction="False"> + <HeaderCustomScript>!include "helpers\StrSlash.nsh"</HeaderCustomScript> + <CreateFunctionCustomScript>${StrSlash} '$0' $INSTDIR + + ${StrSlash} '$1' $_JELLYFINDATADIR_ + + ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \ + \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \ + Installation Folder:\b0 $0\line\b \ + Service install:\b0 $_INSTALLSERVICE_\line\b \ + Service start:\b0 $_SERVICESTART_\line\b \ + Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \ + Jellyfin Data Folder:\b0 $1\par \ +\ + \pard\sa200\sl276\slmult1\f1\lang1043\par \ + }"</CreateFunctionCustomScript> + <RichText Name="ConfirmRichText" Location="12, 12" Size="426, 204" TabIndex="0" ExStyle="WS_EX_STATICEDGE" /> +</Dialog> diff --git a/windows/dialogs/confirmation.nsdinc b/windows/dialogs/confirmation.nsdinc new file mode 100644 index 0000000000..f00e9b43ab --- /dev/null +++ b/windows/dialogs/confirmation.nsdinc @@ -0,0 +1,61 @@ +; ========================================================= +; This file was generated by NSISDialogDesigner 1.4.4.0 +; http://coolsoft.altervista.org/nsisdialogdesigner +; +; Do not edit it manually, use NSISDialogDesigner instead! +; Modified by EraYaN (2019-09-01) +; ========================================================= + +; handle variables +Var hCtl_confirmation +Var hCtl_confirmation_ConfirmRichText + +; HeaderCustomScript +!include "helpers\StrSlash.nsh" + + + +; dialog create function +Function fnc_confirmation_Create + + ; === confirmation (type: Dialog) === + nsDialogs::Create 1018 + Pop $hCtl_confirmation + ${If} $hCtl_confirmation == error + Abort + ${EndIf} + !insertmacro MUI_HEADER_TEXT "Confirmation Page" "Please confirm your choices for Jellyfin Server installation" + + ; === ConfirmRichText (type: RichText) === + nsDialogs::CreateControl /NOUNLOAD "RichEdit20A" ${ES_READONLY}|${WS_VISIBLE}|${WS_CHILD}|${WS_TABSTOP}|${WS_VSCROLL}|${ES_MULTILINE}|${ES_WANTRETURN} ${WS_EX_STATICEDGE} 8u 7u 280u 126u "" + Pop $hCtl_confirmation_ConfirmRichText + ${NSD_AddExStyle} $hCtl_confirmation_ConfirmRichText ${WS_EX_STATICEDGE} + + ; CreateFunctionCustomScript + ${StrSlash} '$0' $INSTDIR + + ${StrSlash} '$1' $_JELLYFINDATADIR_ + + ${If} $_INSTALLSERVICE_ == "Yes" + ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \ + \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \ + Installation Folder:\b0 $0\line\b \ + Service install:\b0 $_INSTALLSERVICE_\line\b \ + Service start:\b0 $_SERVICESTART_\line\b \ + Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \ + Jellyfin Data Folder:\b0 $1\par \ + \ + \pard\sa200\sl276\slmult1\f1\lang1043\par \ + }" + ${Else} + ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \ + \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \ + Installation Folder:\b0 $0\line\b \ + Service install:\b0 $_INSTALLSERVICE_\line\b \ + Jellyfin Data Folder:\b0 $1\par \ + \ + \pard\sa200\sl276\slmult1\f1\lang1043\par \ + }" + ${EndIf} + +FunctionEnd diff --git a/windows/dialogs/service-config.nsddef b/windows/dialogs/service-config.nsddef new file mode 100644 index 0000000000..3509ada249 --- /dev/null +++ b/windows/dialogs/service-config.nsddef @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +This file was created by NSISDialogDesigner 1.4.4.0 +http://coolsoft.altervista.org/nsisdialogdesigner +Do not edit manually! +--> +<Dialog Name="service_config" Title="CoOnfigure the service" Subtitle="This controls what type of access the server gets to this system." GenerateShowFunction="False"> + <CheckBox Name="StartServiceAfterInstall" Location="12, 192" Size="426, 24" Text="Start Service after Install" Checked="True" TabIndex="0" /> + <Label Name="LocalSystemAccountLabel" Location="12, 115" Size="426, 46" Text="The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary." TabIndex="1" /> + <Label Name="NetworkServiceAccountLabel" Location="12, 39" Size="426, 46" Text="The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service." TabIndex="2" /> + <RadioButton Name="UseLocalSystemAccount" Location="12, 88" Size="426, 24" Text="Use Local System account" TabIndex="3" /> + <RadioButton Name="UseNetworkServiceAccount" Location="12, 12" Size="426, 24" Text="Use Network Service account (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="4" /> +</Dialog> \ No newline at end of file diff --git a/windows/dialogs/service-config.nsdinc b/windows/dialogs/service-config.nsdinc new file mode 100644 index 0000000000..58c350f2ec --- /dev/null +++ b/windows/dialogs/service-config.nsdinc @@ -0,0 +1,56 @@ +; ========================================================= +; This file was generated by NSISDialogDesigner 1.4.4.0 +; http://coolsoft.altervista.org/nsisdialogdesigner +; +; Do not edit it manually, use NSISDialogDesigner instead! +; ========================================================= + +; handle variables +Var hCtl_service_config +Var hCtl_service_config_StartServiceAfterInstall +Var hCtl_service_config_LocalSystemAccountLabel +Var hCtl_service_config_NetworkServiceAccountLabel +Var hCtl_service_config_UseLocalSystemAccount +Var hCtl_service_config_UseNetworkServiceAccount +Var hCtl_service_config_Font1 + + +; dialog create function +Function fnc_service_config_Create + + ; custom font definitions + CreateFont $hCtl_service_config_Font1 "Microsoft Sans Serif" "8.25" "700" + + ; === service_config (type: Dialog) === + nsDialogs::Create 1018 + Pop $hCtl_service_config + ${If} $hCtl_service_config == error + Abort + ${EndIf} + !insertmacro MUI_HEADER_TEXT "Configure the service" "This controls what type of access the server gets to this system." + + ; === StartServiceAfterInstall (type: Checkbox) === + ${NSD_CreateCheckbox} 8u 118u 280u 15u "Start Service after Install" + Pop $hCtl_service_config_StartServiceAfterInstall + ${NSD_Check} $hCtl_service_config_StartServiceAfterInstall + + ; === LocalSystemAccountLabel (type: Label) === + ${NSD_CreateLabel} 8u 71u 280u 28u "The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary." + Pop $hCtl_service_config_LocalSystemAccountLabel + + ; === NetworkServiceAccountLabel (type: Label) === + ${NSD_CreateLabel} 8u 24u 280u 28u "The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service." + Pop $hCtl_service_config_NetworkServiceAccountLabel + + ; === UseLocalSystemAccount (type: RadioButton) === + ${NSD_CreateRadioButton} 8u 54u 280u 15u "Use Local System account" + Pop $hCtl_service_config_UseLocalSystemAccount + ${NSD_AddStyle} $hCtl_service_config_UseLocalSystemAccount ${WS_GROUP} + + ; === UseNetworkServiceAccount (type: RadioButton) === + ${NSD_CreateRadioButton} 8u 7u 280u 15u "Use Network Service account (Recommended)" + Pop $hCtl_service_config_UseNetworkServiceAccount + SendMessage $hCtl_service_config_UseNetworkServiceAccount ${WM_SETFONT} $hCtl_service_config_Font1 0 + ${NSD_Check} $hCtl_service_config_UseNetworkServiceAccount + +FunctionEnd diff --git a/windows/dialogs/setuptype.nsddef b/windows/dialogs/setuptype.nsddef new file mode 100644 index 0000000000..b55ceeaaa6 --- /dev/null +++ b/windows/dialogs/setuptype.nsddef @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +This file was created by NSISDialogDesigner 1.4.4.0 +http://coolsoft.altervista.org/nsisdialogdesigner +Do not edit manually! +--> +<Dialog Name="setuptype" Title="Setup Type" Subtitle="Control how Jellyfin is installed."> + <Label Name="InstallasaServiceLabel" Location="12, 115" Size="426, 46" Text="Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares." TabIndex="0" /> + <RadioButton Name="InstallasaService" Location="12, 88" Size="426, 24" Text="Install as a Service (Advanced Users)" TabIndex="1" /> + <Label Name="BasicInstallLabel" Location="12, 39" Size="426, 46" Text="The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4." TabIndex="2" /> + <RadioButton Name="BasicInstall" Location="12, 12" Size="426, 24" Text="Basic Install (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="3" /> +</Dialog> \ No newline at end of file diff --git a/windows/dialogs/setuptype.nsdinc b/windows/dialogs/setuptype.nsdinc new file mode 100644 index 0000000000..8746ad2cc6 --- /dev/null +++ b/windows/dialogs/setuptype.nsdinc @@ -0,0 +1,50 @@ +; ========================================================= +; This file was generated by NSISDialogDesigner 1.4.4.0 +; http://coolsoft.altervista.org/nsisdialogdesigner +; +; Do not edit it manually, use NSISDialogDesigner instead! +; ========================================================= + +; handle variables +Var hCtl_setuptype +Var hCtl_setuptype_InstallasaServiceLabel +Var hCtl_setuptype_InstallasaService +Var hCtl_setuptype_BasicInstallLabel +Var hCtl_setuptype_BasicInstall +Var hCtl_setuptype_Font1 + + +; dialog create function +Function fnc_setuptype_Create + + ; custom font definitions + CreateFont $hCtl_setuptype_Font1 "Microsoft Sans Serif" "8.25" "700" + + ; === setuptype (type: Dialog) === + nsDialogs::Create 1018 + Pop $hCtl_setuptype + ${If} $hCtl_setuptype == error + Abort + ${EndIf} + !insertmacro MUI_HEADER_TEXT "Setup Type" "Control how Jellyfin is installed." + + ; === InstallasaServiceLabel (type: Label) === + ${NSD_CreateLabel} 8u 71u 280u 28u "Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares." + Pop $hCtl_setuptype_InstallasaServiceLabel + + ; === InstallasaService (type: RadioButton) === + ${NSD_CreateRadioButton} 8u 54u 280u 15u "Install as a Service (Advanced Users)" + Pop $hCtl_setuptype_InstallasaService + ${NSD_AddStyle} $hCtl_setuptype_InstallasaService ${WS_GROUP} + + ; === BasicInstallLabel (type: Label) === + ${NSD_CreateLabel} 8u 24u 280u 28u "The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4." + Pop $hCtl_setuptype_BasicInstallLabel + + ; === BasicInstall (type: RadioButton) === + ${NSD_CreateRadioButton} 8u 7u 280u 15u "Basic Install (Recommended)" + Pop $hCtl_setuptype_BasicInstall + SendMessage $hCtl_setuptype_BasicInstall ${WM_SETFONT} $hCtl_setuptype_Font1 0 + ${NSD_Check} $hCtl_setuptype_BasicInstall + +FunctionEnd diff --git a/windows/helpers/ShowError.nsh b/windows/helpers/ShowError.nsh new file mode 100644 index 0000000000..6e09b1e407 --- /dev/null +++ b/windows/helpers/ShowError.nsh @@ -0,0 +1,10 @@ +; Show error +!macro ShowError TEXT RETRYLABEL + MessageBox MB_ABORTRETRYIGNORE|MB_ICONSTOP "${TEXT}" IDIGNORE +2 IDRETRY ${RETRYLABEL} + Abort +!macroend + +!macro ShowErrorFinal TEXT + MessageBox MB_OK|MB_ICONSTOP "${TEXT}" + Abort +!macroend diff --git a/windows/helpers/StrSlash.nsh b/windows/helpers/StrSlash.nsh new file mode 100644 index 0000000000..b8aa771aa6 --- /dev/null +++ b/windows/helpers/StrSlash.nsh @@ -0,0 +1,47 @@ +; Adapted from: https://nsis.sourceforge.io/Another_String_Replace_(and_Slash/BackSlash_Converter) (2019-08-31) + +!macro _StrSlashConstructor out in + Push "${in}" + Push "\" + Call StrSlash + Pop ${out} +!macroend + +!define StrSlash '!insertmacro "_StrSlashConstructor"' + +; Push $filenamestring (e.g. 'c:\this\and\that\filename.htm') +; Push "\" +; Call StrSlash +; Pop $R0 +; ;Now $R0 contains 'c:/this/and/that/filename.htm' +Function StrSlash + Exch $R3 ; $R3 = needle ("\" or "/") + Exch + Exch $R1 ; $R1 = String to replacement in (haystack) + Push $R2 ; Replaced haystack + Push $R4 ; $R4 = not $R3 ("/" or "\") + Push $R6 + Push $R7 ; Scratch reg + StrCpy $R2 "" + StrLen $R6 $R1 + StrCpy $R4 "\" + StrCmp $R3 "/" loop + StrCpy $R4 "/" +loop: + StrCpy $R7 $R1 1 + StrCpy $R1 $R1 $R6 1 + StrCmp $R7 $R3 found + StrCpy $R2 "$R2$R7" + StrCmp $R1 "" done loop +found: + StrCpy $R2 "$R2$R4" + StrCmp $R1 "" done loop +done: + StrCpy $R3 $R2 + Pop $R7 + Pop $R6 + Pop $R4 + Pop $R2 + Pop $R1 + Exch $R3 +FunctionEnd diff --git a/windows/jellyfin.nsi b/windows/jellyfin.nsi new file mode 100644 index 0000000000..fada62d981 --- /dev/null +++ b/windows/jellyfin.nsi @@ -0,0 +1,575 @@ +!verbose 3 +SetCompressor /SOLID bzip2 +ShowInstDetails show +ShowUninstDetails show +Unicode True + +;-------------------------------- +!define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh + + !include "MUI2.nsh" + !include "Sections.nsh" + !include "LogicLib.nsh" + + !include "helpers\ShowError.nsh" + +; Global variables that we'll use + Var _JELLYFINVERSION_ + Var _JELLYFINDATADIR_ + Var _SETUPTYPE_ + Var _INSTALLSERVICE_ + Var _SERVICESTART_ + Var _SERVICEACCOUNTTYPE_ + Var _EXISTINGINSTALLATION_ + Var _EXISTINGSERVICE_ + Var _MAKESHORTCUTS_ + Var _FOLDEREXISTS_ +; +!ifdef x64 + !define ARCH "x64" + !define NAMESUFFIX "(64 bit)" + !define INSTALL_DIRECTORY "$PROGRAMFILES64\Jellyfin\Server" +!endif + +!ifdef x84 + !define ARCH "x86" + !define NAMESUFFIX "(32 bit)" + !define INSTALL_DIRECTORY "$PROGRAMFILES32\Jellyfin\Server" +!endif + +!ifndef ARCH + !error "Set the Arch with /Dx86 or /Dx64" +!endif + +;-------------------------------- + + !define REG_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\JellyfinServer" ;Registry to show up in Add/Remove Programs + !define REG_CONFIG_KEY "Software\Jellyfin\Server" ;Registry to store all configuration + + !getdllversion "$%InstallLocation%\jellyfin.dll" ver_ ;Align installer version with jellyfin.dll version + + Name "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} ${NAMESUFFIX}" ; This is referred in various header text labels + OutFile "jellyfin_${ver_1}.${ver_2}.${ver_3}_windows-${ARCH}.exe" ; Naming convention jellyfin_{version}_windows-{arch].exe + BrandingText "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} Installer" ; This shows in just over the buttons + +; installer attributes, these show up in details tab on installer properties + VIProductVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIProductVersion format, should be X.X.X.X + VIFileVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIFileVersion format, should be X.X.X.X + VIAddVersionKey "ProductName" "Jellyfin Server" + VIAddVersionKey "FileVersion" "${ver_1}.${ver_2}.${ver_3}.0" + VIAddVersionKey "LegalCopyright" "(c) 2019 Jellyfin Contributors. Code released under the GNU General Public License" + VIAddVersionKey "FileDescription" "Jellyfin Server: The Free Software Media System" + +;TODO, check defaults + InstallDir ${INSTALL_DIRECTORY} ;Default installation folder + InstallDirRegKey HKLM "${REG_CONFIG_KEY}" "InstallFolder" ;Read the registry for install folder, + + RequestExecutionLevel admin ; ask it upfront for service control, and installing in priv folders + + CRCCheck on ; make sure the installer wasn't corrupted while downloading + + !define MUI_ABORTWARNING ;Prompts user in case of aborting install + +; TODO: Replace with nice Jellyfin Icons +!ifdef UXPATH + !define MUI_ICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Installer Icon + !define MUI_UNICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Uninstaller Icon + + !define MUI_HEADERIMAGE + !define MUI_HEADERIMAGE_BITMAP "${UXPATH}\branding\NSIS\installer-header.bmp" + !define MUI_WELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp" + !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp" +!endif + +;-------------------------------- +;Pages + +; Welcome Page + !define MUI_WELCOMEPAGE_TEXT "The installer will ask for details to install Jellyfin Server." + !insertmacro MUI_PAGE_WELCOME +; License Page + !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL + +; Setup Type Page + Page custom ShowSetupTypePage SetupTypePage_Config + +; Components Page + !define MUI_PAGE_CUSTOMFUNCTION_PRE HideComponentsPage + !insertmacro MUI_PAGE_COMPONENTS + !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show + !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog + !insertmacro MUI_PAGE_DIRECTORY + +; Data folder Page + !define MUI_PAGE_CUSTOMFUNCTION_PRE HideDataDirectoryPage ; Controls when to hide / show + !define MUI_PAGE_HEADER_TEXT "Choose Data Location" + !define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install the Jellyfin Server data." + !define MUI_DIRECTORYPAGE_TEXT_TOP "The installer will set the following folder for Jellyfin Server data. To install in a different folder, click Browse and select another folder. Please make sure the folder exists and is accessible. Click Next to continue." + !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data folder" + !define MUI_DIRECTORYPAGE_VARIABLE $_JELLYFINDATADIR_ + !insertmacro MUI_PAGE_DIRECTORY + +; Custom Dialogs + !include "dialogs\setuptype.nsdinc" + !include "dialogs\service-config.nsdinc" + !include "dialogs\confirmation.nsdinc" + +; Select service account type + #!define MUI_PAGE_CUSTOMFUNCTION_PRE HideServiceConfigPage ; Controls when to hide / show (This does not work for Page, might need to go PageEx) + #!define MUI_PAGE_CUSTOMFUNCTION_SHOW fnc_service_config_Show + #!define MUI_PAGE_CUSTOMFUNCTION_LEAVE ServiceConfigPage_Config + #!insertmacro MUI_PAGE_CUSTOM ServiceAccountType + Page custom ShowServiceConfigPage ServiceConfigPage_Config + +; Confirmation Page + Page custom ShowConfirmationPage ; just letting the user know what they chose to install + +; Actual Installion Page + !insertmacro MUI_PAGE_INSTFILES + + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + #!insertmacro MUI_UNPAGE_FINISH + +;-------------------------------- +;Languages; Add more languages later here if needed + !insertmacro MUI_LANGUAGE "English" + +;-------------------------------- +;Installer Sections +Section "!Jellyfin Server (required)" InstallJellyfinServer + SectionIn RO ; Mandatory section, isn't this the whole purpose to run the installer. + + StrCmp "$_EXISTINGINSTALLATION_" "Yes" RunUninstaller CarryOn ; Silently uninstall in case of previous installation + + RunUninstaller: + DetailPrint "Looking for uninstaller at $INSTDIR" + FindFirst $0 $1 "$INSTDIR\Uninstall.exe" + FindClose $0 + StrCmp $1 "" CarryOn ; the registry key was there but uninstaller was not found + + DetailPrint "Silently running the uninstaller at $INSTDIR" + ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR' $0 + DetailPrint "Uninstall finished, $0" + + CarryOn: + ${If} $_EXISTINGSERVICE_ == 'Yes' + ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0 + ${If} $0 <> 0 + MessageBox MB_OK|MB_ICONSTOP "Could not stop the Jellyfin Server service." + Abort + ${EndIf} + DetailPrint "Stopped Jellyfin Server service, $0" + ${EndIf} + + SetOutPath "$INSTDIR" + + File "/oname=icon.ico" "${UXPATH}\branding\NSIS\modern-install.ico" + File /r $%InstallLocation%\* + + +; Write the InstallFolder, DataFolder, Network Service info into the registry for later use + WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "InstallFolder" "$INSTDIR" + WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "DataFolder" "$_JELLYFINDATADIR_" + WriteRegStr HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" "$_SERVICEACCOUNTTYPE_" + + !getdllversion "$%InstallLocation%\jellyfin.dll" ver_ + StrCpy $_JELLYFINVERSION_ "${ver_1}.${ver_2}.${ver_3}" ; + +; Write the uninstall keys for Windows + WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayName" "Jellyfin Server $_JELLYFINVERSION_ ${NAMESUFFIX}" + WriteRegExpandStr HKLM "${REG_UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"' + WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Uninstall.exe",0' + WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin Project" + WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.org/" + WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_" + WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1 + WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1 + +;Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" +SectionEnd + +Section "Jellyfin Server Service" InstallService +${If} $_INSTALLSERVICE_ == "Yes" ; Only run this if we're going to install the service! + ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 + DetailPrint "Jellyfin Server service statuscode, $0" + ${If} $0 == 0 + InstallRetry: + ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --service --datadir \"$_JELLYFINDATADIR_\"' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not install the Jellyfin Server service." InstallRetry + ${EndIf} + DetailPrint "Jellyfin Server Service install, $0" + ${Else} + DetailPrint "Jellyfin Server Service exists, updating..." + + ConfigureApplicationRetry: + ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Application "$INSTDIR\jellyfin.exe"' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureApplicationRetry + ${EndIf} + DetailPrint "Jellyfin Server Service setting (Application), $0" + + ConfigureAppParametersRetry: + ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppParameters --service --datadir \"$_JELLYFINDATADIR_\"' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureAppParametersRetry + ${EndIf} + DetailPrint "Jellyfin Server Service setting (AppParameters), $0" + ${EndIf} + + + Sleep 3000 ; Give time for Windows to catchup + ConfigureStartRetry: + ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Start SERVICE_DELAYED_AUTO_START' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureStartRetry + ${EndIf} + DetailPrint "Jellyfin Server Service setting (Start), $0" + + ConfigureDescriptionRetry: + ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Description "Jellyfin Server: The Free Software Media System"' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDescriptionRetry + ${EndIf} + DetailPrint "Jellyfin Server Service setting (Description), $0" + ConfigureDisplayNameRetry: + ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer DisplayName "Jellyfin Server"' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDisplayNameRetry + + ${EndIf} + DetailPrint "Jellyfin Server Service setting (DisplayName), $0" + + Sleep 3000 + ${If} $_SERVICEACCOUNTTYPE_ == "NetworkService" ; the default install using NSSM is Local System + ConfigureNetworkServiceRetry: + ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Objectname "Network Service"' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not configure the Jellyfin Server service account." ConfigureNetworkServiceRetry + ${EndIf} + DetailPrint "Jellyfin Server service account change, $0" + ${EndIf} + + Sleep 3000 + ConfigureDefaultAppExit: + ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppExit Default Exit' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not configure the Jellyfin Server service app exit action." ConfigureDefaultAppExit + ${EndIf} + DetailPrint "Jellyfin Server service exit action set, $0" +${EndIf} + +SectionEnd + +Section "-start service" StartService +${If} $_SERVICESTART_ == "Yes" +${AndIf} $_INSTALLSERVICE_ == "Yes" + StartRetry: + ExecWait '"$INSTDIR\nssm.exe" start JellyfinServer' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not start the Jellyfin Server service." StartRetry + ${EndIf} + DetailPrint "Jellyfin Server service start, $0" +${EndIf} +SectionEnd + +Section "Create Shortcuts" CreateWinShortcuts + ${If} $_MAKESHORTCUTS_ == "Yes" + CreateDirectory "$SMPROGRAMS\Jellyfin Server" + CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin (View Console).lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMAXIMIZED + CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin Tray App.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0 + ;CreateShortCut "$DESKTOP\Jellyfin Server.lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMINIMIZED + CreateShortCut "$DESKTOP\Jellyfin Server\Jellyfin Server.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0 + ${EndIf} +SectionEnd + +;-------------------------------- +;Descriptions + +;Language strings + LangString DESC_InstallJellyfinServer ${LANG_ENGLISH} "Install Jellyfin Server" + LangString DESC_InstallService ${LANG_ENGLISH} "Install As a Service" + +;Assign language strings to sections + !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + !insertmacro MUI_DESCRIPTION_TEXT ${InstallJellyfinServer} $(DESC_InstallJellyfinServer) + !insertmacro MUI_DESCRIPTION_TEXT ${InstallService} $(DESC_InstallService) + !insertmacro MUI_FUNCTION_DESCRIPTION_END + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + + ReadRegStr $INSTDIR HKLM "${REG_CONFIG_KEY}" "InstallFolder" ; read the installation folder + ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; read the data folder + ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; read the account name + + DetailPrint "Jellyfin Install location: $INSTDIR" + DetailPrint "Jellyfin Data folder: $_JELLYFINDATADIR_" + + MessageBox MB_YESNO|MB_ICONINFORMATION "Do you want to retain the Jellyfin Server data folder? The media will not be touched. $\r$\nIf unsure choose YES." /SD IDYES IDYES PreserveData + + RMDir /r /REBOOTOK "$_JELLYFINDATADIR_" + + PreserveData: + + ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 + DetailPrint "Jellyfin Server service statuscode, $0" + IntCmp $0 0 NoServiceUninstall ; service doesn't exist, may be run from desktop shortcut + + Sleep 3000 ; Give time for Windows to catchup + + UninstallStopRetry: + ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not stop the Jellyfin Server service." UninstallStopRetry + ${EndIf} + DetailPrint "Stopped Jellyfin Server service, $0" + + UninstallRemoveRetry: + ExecWait '"$INSTDIR\nssm.exe" remove JellyfinServer confirm' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not remove the Jellyfin Server service." UninstallRemoveRetry + ${EndIf} + DetailPrint "Removed Jellyfin Server service, $0" + + Sleep 3000 ; Give time for Windows to catchup + + NoServiceUninstall: ; existing install was present but no service was detected. Remove shortcuts if account is set to none + ${If} $_SERVICEACCOUNTTYPE_ == "None" + RMDir /r "$SMPROGRAMS\Jellyfin Server" + Delete "$DESKTOP\Jellyfin Server.lnk" + DetailPrint "Removed old shortcuts..." + ${EndIf} + + Delete "$INSTDIR\*.*" + RMDir /r /REBOOTOK "$INSTDIR\jellyfin-web" + Delete "$INSTDIR\Uninstall.exe" + RMDir /r /REBOOTOK "$INSTDIR" + + DeleteRegKey HKLM "Software\Jellyfin" + DeleteRegKey HKLM "${REG_UNINST_KEY}" + +SectionEnd + +Function .onInit +; Setting up defaults + StrCpy $_INSTALLSERVICE_ "Yes" + StrCpy $_SERVICESTART_ "Yes" + StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService" + StrCpy $_EXISTINGINSTALLATION_ "No" + StrCpy $_EXISTINGSERVICE_ "No" + StrCpy $_MAKESHORTCUTS_ "No" + + SetShellVarContext current + StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server" + + System::Call 'kernel32::CreateMutex(p 0, i 0, t "JellyfinServerMutex") p .r1 ?e' + Pop $R0 + + StrCmp $R0 0 +3 + !insertmacro ShowErrorFinal "The installer is already running." + +;Detect if Jellyfin is already installed. +; In case it is installed, let the user choose either +; 1. Exit installer +; 2. Upgrade without messing with data +; 2a. Don't ask for any details, uninstall and install afresh with old settings + +; Read Registry for previous installation + ClearErrors + ReadRegStr "$0" HKLM "${REG_CONFIG_KEY}" "InstallFolder" + IfErrors NoExisitingInstall + + DetailPrint "Existing Jellyfin Server detected at: $0" + StrCpy "$INSTDIR" "$0" ; set the location fro registry as new default + + StrCpy $_EXISTINGINSTALLATION_ "Yes" ; Set our flag to be used later + SectionSetText ${InstallJellyfinServer} "Upgrade Jellyfin Server (required)" ; Change install text to "Upgrade" + + ; check if service was run using Network Service account + ClearErrors + ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default + + ClearErrors + ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds + + ; Hide sections which will not be needed in case of previous install + ; SectionSetText ${InstallService} "" + +; check if there is a service called Jellyfin, there should be +; hack : nssm statuscode Jellyfin will return non zero return code in case it exists + ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 + DetailPrint "Jellyfin Server service statuscode, $0" + IntCmp $0 0 NoService ; service doesn't exist, may be run from desktop shortcut + + ; if service was detected, set defaults going forward. + StrCpy $_EXISTINGSERVICE_ "Yes" + StrCpy $_INSTALLSERVICE_ "Yes" + StrCpy $_SERVICESTART_ "Yes" + StrCpy $_MAKESHORTCUTS_ "No" + SectionSetText ${CreateWinShortcuts} "" + + + NoService: ; existing install was present but no service was detected + ${If} $_SERVICEACCOUNTTYPE_ == "None" + StrCpy $_SETUPTYPE_ "Basic" + StrCpy $_INSTALLSERVICE_ "No" + StrCpy $_SERVICESTART_ "No" + StrCpy $_MAKESHORTCUTS_ "Yes" + ${EndIf} + +; Let the user know that we'll upgrade and provide an option to quit. + MessageBox MB_OKCANCEL|MB_ICONINFORMATION "Existing installation of Jellyfin Server was detected, it'll be upgraded, settings will be retained. \ + $\r$\nClick OK to proceed, Cancel to exit installer." /SD IDOK IDOK ProceedWithUpgrade + Quit ; Quit if the user is not sure about upgrade + + ProceedWithUpgrade: + + NoExisitingInstall: ; by this time, the variables have been correctly set to reflect previous install details + +FunctionEnd + +Function HideInstallDirectoryPage + ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder + Abort + ${EndIf} +FunctionEnd + +Function HideDataDirectoryPage + ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder + Abort + ${EndIf} +FunctionEnd + +Function HideServiceConfigPage + ${If} $_INSTALLSERVICE_ == "No" ; Not running as a service, don't ask for service type + ${OrIf} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder + Abort + ${EndIf} +FunctionEnd + +Function HideConfirmationPage + ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder + Abort + ${EndIf} +FunctionEnd + +Function HideSetupTypePage + ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for SetupType + Abort + ${EndIf} +FunctionEnd + +Function HideComponentsPage + ${If} $_SETUPTYPE_ == "Basic" ; Basic installation chosen, don't show components choice + Abort + ${EndIf} +FunctionEnd + +; Setup Type dialog show function +Function ShowSetupTypePage + Call HideSetupTypePage + Call fnc_setuptype_Create + nsDialogs::Show +FunctionEnd + +; Service Config dialog show function +Function ShowServiceConfigPage + Call HideServiceConfigPage + Call fnc_service_config_Create + nsDialogs::Show +FunctionEnd + +; Confirmation dialog show function +Function ShowConfirmationPage + Call HideConfirmationPage + Call fnc_confirmation_Create + nsDialogs::Show +FunctionEnd + +; Declare temp variables to read the options from the custom page. +Var StartServiceAfterInstall +Var UseNetworkServiceAccount +Var UseLocalSystemAccount +Var BasicInstall + + +Function SetupTypePage_Config +${NSD_GetState} $hCtl_setuptype_BasicInstall $BasicInstall + IfFileExists "$LOCALAPPDATA\Jellyfin" folderfound foldernotfound ; if the folder exists, use this, otherwise, go with new default + folderfound: + StrCpy $_FOLDEREXISTS_ "Yes" + Goto InstallCheck + foldernotfound: + StrCpy $_FOLDEREXISTS_ "No" + Goto InstallCheck + +InstallCheck: +${If} $BasicInstall == 1 + StrCpy $_SETUPTYPE_ "Basic" + StrCpy $_INSTALLSERVICE_ "No" + StrCpy $_SERVICESTART_ "No" + StrCpy $_SERVICEACCOUNTTYPE_ "None" + StrCpy $_MAKESHORTCUTS_ "Yes" + ${If} $_FOLDEREXISTS_ == "Yes" + StrCpy $_JELLYFINDATADIR_ "$LOCALAPPDATA\Jellyfin\" + ${EndIf} +${Else} + StrCpy $_SETUPTYPE_ "Advanced" + StrCpy $_INSTALLSERVICE_ "Yes" + StrCpy $_MAKESHORTCUTS_ "No" + ${If} $_FOLDEREXISTS_ == "Yes" + MessageBox MB_OKCANCEL|MB_ICONINFORMATION "An existing data folder was detected.\ + $\r$\nBasic Setup is highly recommended.\ + $\r$\nIf you proceed, you will need to set up Jellyfin again." IDOK GoAhead IDCANCEL GoBack + GoBack: + Abort + ${EndIf} + GoAhead: + StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server" + SectionSetText ${CreateWinShortcuts} "" +${EndIf} + +FunctionEnd + +Function ServiceConfigPage_Config +${NSD_GetState} $hCtl_service_config_StartServiceAfterInstall $StartServiceAfterInstall +${If} $StartServiceAfterInstall == 1 + StrCpy $_SERVICESTART_ "Yes" +${Else} + StrCpy $_SERVICESTART_ "No" +${EndIf} +${NSD_GetState} $hCtl_service_config_UseNetworkServiceAccount $UseNetworkServiceAccount +${NSD_GetState} $hCtl_service_config_UseLocalSystemAccount $UseLocalSystemAccount + +${If} $UseNetworkServiceAccount == 1 + StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService" +${ElseIf} $UseLocalSystemAccount == 1 + StrCpy $_SERVICEACCOUNTTYPE_ "LocalSystem" +${Else} + !insertmacro ShowErrorFinal "Service account type not properly configured." +${EndIf} + +FunctionEnd + +; This function handles the choices during component selection +Function .onSelChange + +; If we are not installing service, we don't need to set the NetworkService account or StartService + SectionGetFlags ${InstallService} $0 + ${If} $0 = ${SF_SELECTED} + StrCpy $_INSTALLSERVICE_ "Yes" + ${Else} + StrCpy $_INSTALLSERVICE_ "No" + StrCpy $_SERVICESTART_ "No" + StrCpy $_SERVICEACCOUNTTYPE_ "None" + ${EndIf} +FunctionEnd + +Function .onInstSuccess + #ExecShell "open" "http://localhost:8096" +FunctionEnd diff --git a/windows/legacy/install-jellyfin.ps1 b/windows/legacy/install-jellyfin.ps1 new file mode 100644 index 0000000000..e909a0468e --- /dev/null +++ b/windows/legacy/install-jellyfin.ps1 @@ -0,0 +1,460 @@ +[CmdletBinding()] + +param( + [Switch]$Quiet, + [Switch]$InstallAsService, + [System.Management.Automation.pscredential]$ServiceUser, + [switch]$CreateDesktopShorcut, + [switch]$LaunchJellyfin, + [switch]$MigrateEmbyLibrary, + [string]$InstallLocation, + [string]$EmbyLibraryLocation, + [string]$JellyfinLibraryLocation +) +<# This form was created using POSHGUI.com a free online gui designer for PowerShell +.NAME + Install-Jellyfin +#> + +#This doesn't need to be used by default anymore, but I am keeping it in as a function for future use. +function Elevate-Window { + # Get the ID and security principal of the current user account + $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent() + $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID) + + # Get the security principal for the Administrator role + $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator + + # Check to see if we are currently running "as Administrator" + if ($myWindowsPrincipal.IsInRole($adminRole)) + { + # We are running "as Administrator" - so change the title and background color to indicate this + $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)" + $Host.UI.RawUI.BackgroundColor = "DarkBlue" + clear-host + } + else + { + # We are not running "as Administrator" - so relaunch as administrator + + # Create a new process object that starts PowerShell + $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell"; + + # Specify the current script path and name as a parameter + $newProcess.Arguments = $myInvocation.MyCommand.Definition; + + # Indicate that the process should be elevated + $newProcess.Verb = "runas"; + + # Start the new process + [System.Diagnostics.Process]::Start($newProcess); + + # Exit from the current, unelevated, process + exit + } +} + +#FIXME The install methods should be a function that takes all the params, the quiet flag should be a paramset + +if($Quiet.IsPresent -or $Quiet -eq $true){ + if([string]::IsNullOrEmpty($JellyfinLibraryLocation)){ + $Script:JellyfinDataDir = "$env:LOCALAPPDATA\jellyfin\" + }else{ + $Script:JellyfinDataDir = $JellyfinLibraryLocation + } + if([string]::IsNullOrEmpty($InstallLocation)){ + $Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\" + }else{ + $Script:DefaultJellyfinInstallDirectory = $InstallLocation + } + + if([string]::IsNullOrEmpty($EmbyLibraryLocation)){ + $Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\data\" + }else{ + $Script:defaultEmbyDataDir = $EmbyLibraryLocation + } + + if($InstallAsService.IsPresent -or $InstallAsService -eq $true){ + $Script:InstallAsService = $true + }else{$Script:InstallAsService = $false} + if($null -eq $ServiceUser){ + $Script:InstallServiceAsUser = $false + }else{ + $Script:InstallServiceAsUser = $true + $Script:UserCredentials = $ServiceUser + $Script:JellyfinDataDir = "$env:HOMEDRIVE\Users\$($Script:UserCredentials.UserName)\Appdata\Local\jellyfin\"} + if($CreateDesktopShorcut.IsPresent -or $CreateDesktopShorcut -eq $true) {$Script:CreateShortcut = $true}else{$Script:CreateShortcut = $false} + if($MigrateEmbyLibrary.IsPresent -or $MigrateEmbyLibrary -eq $true){$Script:MigrateLibrary = $true}else{$Script:MigrateLibrary = $false} + if($LaunchJellyfin.IsPresent -or $LaunchJellyfin -eq $true){$Script:StartJellyfin = $true}else{$Script:StartJellyfin = $false} + + if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){ + mkdir $Script:DefaultJellyfinInstallDirectory + } + Copy-Item -Path $PSScriptRoot/* -DestinationPath "$Script:DefaultJellyfinInstallDirectory/" -Force -Recurse + if($Script:InstallAsService){ + if($Script:InstallServiceAsUser){ + &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" + Start-Sleep -Milliseconds 500 + &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)" + &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START + }else{ + &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" + Start-Sleep -Milliseconds 500 + #&"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin ObjectName $Script:UserCredentials.UserName $Script:UserCredentials.GetNetworkCredential().Password + #Set-Service -Name Jellyfin -Credential $Script:UserCredentials + &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START + } + } + if($Script:MigrateLibrary){ + Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse + Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse + Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse + Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse + Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse + } + if($Script:CreateShortcut){ + $WshShell = New-Object -comObject WScript.Shell + $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk") + $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe" + $Shortcut.Save() + } + if($Script:StartJellyfin){ + if($Script:InstallAsService){ + Get-Service Jellyfin | Start-Service + }else{ + Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru + } + } +}else{ + +} +Add-Type -AssemblyName System.Windows.Forms +[System.Windows.Forms.Application]::EnableVisualStyles() + +$Script:JellyFinDataDir = "$env:LOCALAPPDATA\jellyfin\" +$Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\" +$Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\" +$Script:InstallAsService = $False +$Script:InstallServiceAsUser = $false +$Script:CreateShortcut = $false +$Script:MigrateLibrary = $false +$Script:StartJellyfin = $false + +function InstallJellyfin { + Write-Host "Install as service: $Script:InstallAsService" + Write-Host "Install as serviceuser: $Script:InstallServiceAsUser" + Write-Host "Create Shortcut: $Script:CreateShortcut" + Write-Host "MigrateLibrary: $Script:MigrateLibrary" + $GUIElementsCollection | ForEach-Object { + $_.Enabled = $false + } + Write-Host "Making Jellyfin directory" + $ProgressBar.Minimum = 1 + $ProgressBar.Maximum = 100 + $ProgressBar.Value = 1 + if($Script:DefaultJellyfinInstallDirectory -ne $InstallLocationBox.Text){ + Write-Host "Custom Install Location Chosen: $($InstallLocationBox.Text)" + $Script:DefaultJellyfinInstallDirectory = $InstallLocationBox.Text + } + if($Script:JellyfinDataDir -ne $CustomLibraryBox.Text){ + Write-Host "Custom Library Location Chosen: $($CustomLibraryBox.Text)" + $Script:JellyfinDataDir = $CustomLibraryBox.Text + } + if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){ + mkdir $Script:DefaultJellyfinInstallDirectory + } + Write-Host "Copying Jellyfin Data" + $progressbar.Value = 10 + Copy-Item -Path $PSScriptRoot/* -Destination $Script:DefaultJellyfinInstallDirectory/ -Force -Recurse + Write-Host "Finished Copying" + $ProgressBar.Value = 50 + if($Script:InstallAsService){ + if($Script:InstallServiceAsUser){ + Write-Host "Installing Service as user $($Script:UserCredentials.UserName)" + &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" + Start-Sleep -Milliseconds 2000 + &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)" + &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START + }else{ + Write-Host "Installing Service as LocalSystem" + &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" + Start-Sleep -Milliseconds 2000 + &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START + } + } + $progressbar.Value = 60 + if($Script:MigrateLibrary){ + if($Script:defaultEmbyDataDir -ne $LibraryLocationBox.Text){ + Write-Host "Custom location defined for emby library: $($LibraryLocationBox.Text)" + $Script:defaultEmbyDataDir = $LibraryLocationBox.Text + } + Write-Host "Copying emby library from $Script:defaultEmbyDataDir to $Script:JellyFinDataDir" + Write-Host "This could take a while depending on the size of your library. Please be patient" + Write-Host "Copying config" + Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse + Write-Host "Copying cache" + Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse + Write-Host "Copying data" + Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse + Write-Host "Copying metadata" + Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse + Write-Host "Copying root dir" + Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse + } + $progressbar.Value = 80 + if($Script:CreateShortcut){ + Write-Host "Creating Shortcut" + $WshShell = New-Object -comObject WScript.Shell + $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk") + $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe" + $Shortcut.Save() + } + $ProgressBar.Value = 90 + if($Script:StartJellyfin){ + if($Script:InstallAsService){ + Write-Host "Starting Jellyfin Service" + Get-Service Jellyfin | Start-Service + }else{ + Write-Host "Starting Jellyfin" + Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru + } + } + $progressbar.Value = 100 + Write-Host Finished + $wshell = New-Object -ComObject Wscript.Shell + $wshell.Popup("Operation Completed",0,"Done",0x1) + $InstallForm.Close() +} +function ServiceBoxCheckChanged { + if($InstallAsServiceCheck.Checked){ + $Script:InstallAsService = $true + $ServiceUserLabel.Visible = $true + $ServiceUserLabel.Enabled = $true + $ServiceUserBox.Visible = $true + $ServiceUserBox.Enabled = $true + }else{ + $Script:InstallAsService = $false + $ServiceUserLabel.Visible = $false + $ServiceUserLabel.Enabled = $false + $ServiceUserBox.Visible = $false + $ServiceUserBox.Enabled = $false + } +} +function UserSelect { + if($ServiceUserBox.Text -eq 'Local System') + { + $Script:InstallServiceAsUser = $false + $Script:UserCredentials = $null + $ServiceUserBox.Items.RemoveAt(1) + $ServiceUserBox.Items.Add("Custom User") + }elseif($ServiceUserBox.Text -eq 'Custom User'){ + $Script:InstallServiceAsUser = $true + $Script:UserCredentials = Get-Credential -Message "Please enter the credentials of the user you with to run Jellyfin Service as" -UserName $env:USERNAME + $ServiceUserBox.Items[1] = "$($Script:UserCredentials.UserName)" + } +} +function CreateShortcutBoxCheckChanged { + if($CreateShortcutCheck.Checked){ + $Script:CreateShortcut = $true + }else{ + $Script:CreateShortcut = $False + } +} +function StartJellyFinBoxCheckChanged { + if($StartProgramCheck.Checked){ + $Script:StartJellyfin = $true + }else{ + $Script:StartJellyfin = $false + } +} + +function CustomLibraryCheckChanged { + if($CustomLibraryCheck.Checked){ + $Script:UseCustomLibrary = $true + $CustomLibraryBox.Enabled = $true + }else{ + $Script:UseCustomLibrary = $false + $CustomLibraryBox.Enabled = $false + } +} + +function MigrateLibraryCheckboxChanged { + + if($MigrateLibraryCheck.Checked){ + $Script:MigrateLibrary = $true + $LibraryMigrationLabel.Visible = $true + $LibraryMigrationLabel.Enabled = $true + $LibraryLocationBox.Visible = $true + $LibraryLocationBox.Enabled = $true + }else{ + $Script:MigrateLibrary = $false + $LibraryMigrationLabel.Visible = $false + $LibraryMigrationLabel.Enabled = $false + $LibraryLocationBox.Visible = $false + $LibraryLocationBox.Enabled = $false + } + +} + + +#region begin GUI{ + +$InstallForm = New-Object system.Windows.Forms.Form +$InstallForm.ClientSize = '320,240' +$InstallForm.text = "Terrible Jellyfin Installer" +$InstallForm.TopMost = $false + +$GUIElementsCollection = @() + +$InstallButton = New-Object system.Windows.Forms.Button +$InstallButton.text = "Install" +$InstallButton.width = 60 +$InstallButton.height = 30 +$InstallButton.location = New-Object System.Drawing.Point(5,5) +$InstallButton.Font = 'Microsoft Sans Serif,10' +$GUIElementsCollection += $InstallButton + +$ProgressBar = New-Object system.Windows.Forms.ProgressBar +$ProgressBar.width = 245 +$ProgressBar.height = 30 +$ProgressBar.location = New-Object System.Drawing.Point(70,5) + +$InstallLocationLabel = New-Object system.Windows.Forms.Label +$InstallLocationLabel.text = "Install Location" +$InstallLocationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft +$InstallLocationLabel.AutoSize = $true +$InstallLocationLabel.width = 100 +$InstallLocationLabel.height = 20 +$InstallLocationLabel.location = New-Object System.Drawing.Point(5,50) +$InstallLocationLabel.Font = 'Microsoft Sans Serif,10' +$GUIElementsCollection += $InstallLocationLabel + +$InstallLocationBox = New-Object system.Windows.Forms.TextBox +$InstallLocationBox.multiline = $false +$InstallLocationBox.width = 205 +$InstallLocationBox.height = 20 +$InstallLocationBox.location = New-Object System.Drawing.Point(110,50) +$InstallLocationBox.Text = $Script:DefaultJellyfinInstallDirectory +$InstallLocationBox.Font = 'Microsoft Sans Serif,10' +$GUIElementsCollection += $InstallLocationBox + +$CustomLibraryCheck = New-Object system.Windows.Forms.CheckBox +$CustomLibraryCheck.text = "Custom Library Location:" +$CustomLibraryCheck.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft +$CustomLibraryCheck.AutoSize = $false +$CustomLibraryCheck.width = 180 +$CustomLibraryCheck.height = 20 +$CustomLibraryCheck.location = New-Object System.Drawing.Point(5,75) +$CustomLibraryCheck.Font = 'Microsoft Sans Serif,10' +$GUIElementsCollection += $CustomLibraryCheck + +$CustomLibraryBox = New-Object system.Windows.Forms.TextBox +$CustomLibraryBox.multiline = $false +$CustomLibraryBox.width = 130 +$CustomLibraryBox.height = 20 +$CustomLibraryBox.location = New-Object System.Drawing.Point(185,75) +$CustomLibraryBox.Text = $Script:JellyFinDataDir +$CustomLibraryBox.Font = 'Microsoft Sans Serif,10' +$CustomLibraryBox.Enabled = $false +$GUIElementsCollection += $CustomLibraryBox + +$InstallAsServiceCheck = New-Object system.Windows.Forms.CheckBox +$InstallAsServiceCheck.text = "Install as Service" +$InstallAsServiceCheck.AutoSize = $false +$InstallAsServiceCheck.width = 140 +$InstallAsServiceCheck.height = 20 +$InstallAsServiceCheck.location = New-Object System.Drawing.Point(5,125) +$InstallAsServiceCheck.Font = 'Microsoft Sans Serif,10' +$GUIElementsCollection += $InstallAsServiceCheck + +$ServiceUserLabel = New-Object system.Windows.Forms.Label +$ServiceUserLabel.text = "Run Service As:" +$ServiceUserLabel.AutoSize = $true +$ServiceUserLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft +$ServiceUserLabel.width = 100 +$ServiceUserLabel.height = 20 +$ServiceUserLabel.location = New-Object System.Drawing.Point(15,145) +$ServiceUserLabel.Font = 'Microsoft Sans Serif,10' +$ServiceUserLabel.Visible = $false +$ServiceUserLabel.Enabled = $false +$GUIElementsCollection += $ServiceUserLabel + +$ServiceUserBox = New-Object system.Windows.Forms.ComboBox +$ServiceUserBox.text = "Run Service As" +$ServiceUserBox.width = 195 +$ServiceUserBox.height = 20 +@('Local System','Custom User') | ForEach-Object {[void] $ServiceUserBox.Items.Add($_)} +$ServiceUserBox.location = New-Object System.Drawing.Point(120,145) +$ServiceUserBox.Font = 'Microsoft Sans Serif,10' +$ServiceUserBox.Visible = $false +$ServiceUserBox.Enabled = $false +$ServiceUserBox.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList +$GUIElementsCollection += $ServiceUserBox + +$MigrateLibraryCheck = New-Object system.Windows.Forms.CheckBox +$MigrateLibraryCheck.text = "Import Emby/Old JF Library" +$MigrateLibraryCheck.AutoSize = $false +$MigrateLibraryCheck.width = 160 +$MigrateLibraryCheck.height = 20 +$MigrateLibraryCheck.location = New-Object System.Drawing.Point(5,170) +$MigrateLibraryCheck.Font = 'Microsoft Sans Serif,10' +$GUIElementsCollection += $MigrateLibraryCheck + +$LibraryMigrationLabel = New-Object system.Windows.Forms.Label +$LibraryMigrationLabel.text = "Emby/Old JF Library Path" +$LibraryMigrationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft +$LibraryMigrationLabel.AutoSize = $false +$LibraryMigrationLabel.width = 120 +$LibraryMigrationLabel.height = 20 +$LibraryMigrationLabel.location = New-Object System.Drawing.Point(15,190) +$LibraryMigrationLabel.Font = 'Microsoft Sans Serif,10' +$LibraryMigrationLabel.Visible = $false +$LibraryMigrationLabel.Enabled = $false +$GUIElementsCollection += $LibraryMigrationLabel + +$LibraryLocationBox = New-Object system.Windows.Forms.TextBox +$LibraryLocationBox.multiline = $false +$LibraryLocationBox.width = 175 +$LibraryLocationBox.height = 20 +$LibraryLocationBox.location = New-Object System.Drawing.Point(140,190) +$LibraryLocationBox.Text = $Script:defaultEmbyDataDir +$LibraryLocationBox.Font = 'Microsoft Sans Serif,10' +$LibraryLocationBox.Visible = $false +$LibraryLocationBox.Enabled = $false +$GUIElementsCollection += $LibraryLocationBox + +$CreateShortcutCheck = New-Object system.Windows.Forms.CheckBox +$CreateShortcutCheck.text = "Desktop Shortcut" +$CreateShortcutCheck.AutoSize = $false +$CreateShortcutCheck.width = 150 +$CreateShortcutCheck.height = 20 +$CreateShortcutCheck.location = New-Object System.Drawing.Point(5,215) +$CreateShortcutCheck.Font = 'Microsoft Sans Serif,10' +$GUIElementsCollection += $CreateShortcutCheck + +$StartProgramCheck = New-Object system.Windows.Forms.CheckBox +$StartProgramCheck.text = "Start Jellyfin" +$StartProgramCheck.AutoSize = $false +$StartProgramCheck.width = 160 +$StartProgramCheck.height = 20 +$StartProgramCheck.location = New-Object System.Drawing.Point(160,215) +$StartProgramCheck.Font = 'Microsoft Sans Serif,10' +$GUIElementsCollection += $StartProgramCheck + +$InstallForm.controls.AddRange($GUIElementsCollection) +$InstallForm.Controls.Add($ProgressBar) + +#region gui events { +$InstallButton.Add_Click({ InstallJellyfin }) +$CustomLibraryCheck.Add_CheckedChanged({CustomLibraryCheckChanged}) +$InstallAsServiceCheck.Add_CheckedChanged({ServiceBoxCheckChanged}) +$ServiceUserBox.Add_SelectedValueChanged({ UserSelect }) +$MigrateLibraryCheck.Add_CheckedChanged({MigrateLibraryCheckboxChanged}) +$CreateShortcutCheck.Add_CheckedChanged({CreateShortcutBoxCheckChanged}) +$StartProgramCheck.Add_CheckedChanged({StartJellyFinBoxCheckChanged}) +#endregion events } + +#endregion GUI } + + +[void]$InstallForm.ShowDialog() diff --git a/windows/legacy/install.bat b/windows/legacy/install.bat new file mode 100644 index 0000000000..e21479a79a --- /dev/null +++ b/windows/legacy/install.bat @@ -0,0 +1 @@ +powershell.exe -executionpolicy Bypass -file install-jellyfin.ps1