├── .devcontainer ├── devcontainer.env ├── library-scripts │ ├── meta.env │ ├── jellyfin-web-install.sh │ ├── README.md │ ├── azcli-debian.sh │ ├── node-debian.sh │ └── common-debian.sh ├── Dockerfile ├── base.Dockerfile └── devcontainer.json ├── .gitignore ├── src ├── Jellyfin.Extensions │ └── Json │ │ └── Converters │ │ └── JsonNullableStructConverter.cs ├── Jellyfin.Plugin.Kinopoisk │ ├── Configuration │ │ ├── PluginConfiguration.cs │ │ └── configPage.html │ ├── ProviderIdResolvers │ │ ├── IProviderIdResolver.cs │ │ ├── CommonResolver.cs │ │ ├── CommonLookupInfoResolver.cs │ │ └── MovieResolver.cs │ ├── Constants.cs │ ├── Model │ │ ├── KinopoiskPersonExternalId.cs │ │ └── KinopoiskExternalId.cs │ ├── build.yaml │ ├── MetadataProviders │ │ ├── MovieMetadataProvider.cs │ │ ├── SeriesMetadataProvider.cs │ │ ├── BaseMetadataProvider.cs │ │ ├── PersonMetadataProvider.cs │ │ └── BaseVideoMetadataProvider.cs │ ├── Jellyfin.Plugin.Kinopoisk.csproj │ ├── Plugin.cs │ ├── RemoteImageUrlSanitizer.cs │ ├── KinopoiskPluginServiceRegistrator.cs │ ├── RemoteImageProviders │ │ ├── BaseImageProvider.cs │ │ ├── PersonImageProvider.cs │ │ └── VideoImageProvider.cs │ ├── TransliterationStringExtensions.cs │ └── ApiModelExtensions.cs ├── Jellyfin.Plugin.Kinopoisk.Tests │ ├── TransliterationTests.cs │ └── Jellyfin.Plugin.Kinopoisk.Tests.csproj ├── KinopoiskUnofficialInfo.ApiClient │ ├── IKinopoiskApiClient.cs │ ├── Patchers │ │ ├── BadProductionStatusConverter.cs │ │ ├── BadProfessionKeyConverter.cs │ │ ├── JsonTransformator.cs │ │ ├── BadFilmSearchResponse_filmsTypeConverter.cs │ │ ├── BadIntegerConverter.cs │ │ ├── BadDoubleConverter.cs │ │ └── DeclarationPatcherContractResolver.cs │ ├── KinopoiskUnofficialInfo.ApiClient.csproj │ ├── KinopoiskApiClient.cs │ └── CachedApiClient.cs ├── KinopoiskUnofficialInfo.ApiClient.Tests │ ├── cassettes │ │ ├── GetTrailers_ShouldReturnEmptyResponse_948870.yaml │ │ ├── GetTrailers_ShouldParseName_1395460.yaml │ │ ├── GetSingleFilm_ShouldParseFilm_1445243.yaml │ │ ├── GetSingleFilm_ShouldParseTvShow_4416198.yaml │ │ ├── GetSingleFilm_ShouldParseFilm_251733.yaml │ │ ├── GetSingleFilm_ShouldParseFilm_1044982.yaml │ │ ├── GetSingleFilm_ShouldParseFilm_1142206.yaml │ │ ├── GetSingleFilm_ShouldParseTvShow_77298.yaml │ │ ├── GetPerson_ShouldParseName_3873197.yaml │ │ └── GetSingleFilm_ShouldParseProducerUssr_89540.yaml │ ├── KinopoiskUnofficialInfo.ApiClient.Tests.csproj │ └── KinopoiskApiTests.cs └── Jellyfin.Plugin.Kinopoisk.sln ├── dist ├── kinopoisk │ ├── kinopoisk_10.7.0.3.zip │ ├── kinopoisk_10.7.0.4.zip │ ├── kinopoisk_10.7.0.5.zip │ ├── kinopoisk_10.7.5.0.zip │ ├── kinopoisk_10.7.5.1.zip │ ├── kinopoisk_10.7.5.2.zip │ ├── kinopoisk_10.7.5.3.zip │ ├── kinopoisk_10.7.5.4.zip │ ├── kinopoisk_10.7.5.6.zip │ ├── kinopoisk_10.7.5.7.zip │ ├── kinopoisk_10.8.9.0.zip │ ├── kinopoisk_10.8.9.1.zip │ ├── kinopoisk_10.8.9.2.zip │ ├── kinopoisk_10.8.9.3.zip │ ├── kinopoisk_10.9.0.0.zip │ ├── kinopoisk_10.9.0.1.zip │ ├── jellyfin-plugin-kinopoisk_10.6.0.0.zip │ ├── jellyfin-plugin-kinopoisk_10.6.0.1.zip │ ├── jellyfin-plugin-kinopoisk_10.6.0.2.zip │ ├── jellyfin-plugin-kinopoisk_10.7.0.0.zip │ ├── jellyfin-plugin-kinopoisk_10.7.0.1.zip │ └── jellyfin-plugin-kinopoisk_10.7.0.2.zip └── manifest.json ├── .github ├── dependabot.yml └── workflows │ └── pull.yaml ├── kinopoisk.code-workspace ├── README.md ├── .vscode ├── launch.json └── tasks.json ├── publish.sh └── .editorconfig /.devcontainer/devcontainer.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/meta.env: -------------------------------------------------------------------------------- 1 | VERSION='dev' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/*/bin/* 2 | src/*/obj/* 3 | .devcontainer 4 | .DS_Store -------------------------------------------------------------------------------- /src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.0.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.0.3.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.0.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.0.4.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.0.5.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.0.5.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.5.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.5.0.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.5.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.5.1.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.5.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.5.2.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.5.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.5.3.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.5.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.5.4.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.5.6.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.5.6.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.7.5.7.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.7.5.7.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.8.9.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.8.9.0.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.8.9.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.8.9.1.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.8.9.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.8.9.2.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.8.9.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.8.9.3.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.9.0.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.9.0.0.zip -------------------------------------------------------------------------------- /dist/kinopoisk/kinopoisk_10.9.0.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/kinopoisk_10.9.0.1.zip -------------------------------------------------------------------------------- /dist/kinopoisk/jellyfin-plugin-kinopoisk_10.6.0.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.6.0.0.zip -------------------------------------------------------------------------------- /dist/kinopoisk/jellyfin-plugin-kinopoisk_10.6.0.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.6.0.1.zip -------------------------------------------------------------------------------- /dist/kinopoisk/jellyfin-plugin-kinopoisk_10.6.0.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.6.0.2.zip -------------------------------------------------------------------------------- /dist/kinopoisk/jellyfin-plugin-kinopoisk_10.7.0.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.7.0.0.zip -------------------------------------------------------------------------------- /dist/kinopoisk/jellyfin-plugin-kinopoisk_10.7.0.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.7.0.1.zip -------------------------------------------------------------------------------- /dist/kinopoisk/jellyfin-plugin-kinopoisk_10.7.0.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/HEAD/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.7.0.2.zip -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/Configuration/PluginConfiguration.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Plugins; 2 | 3 | namespace Jellyfin.Plugin.Kinopoisk.Configuration 4 | { 5 | public class PluginConfiguration : BasePluginConfiguration 6 | { 7 | // https://kinopoiskapiunofficial.tech/ 8 | public string ApiToken { get; set; } = "85d30ae5-d875-4c5f-900d-8e37bb20625e"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/ProviderIdResolvers/IProviderIdResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using MediaBrowser.Model.Entities; 4 | 5 | namespace Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers 6 | { 7 | public interface IProviderIdResolver 8 | where T: IHasProviderIds 9 | { 10 | Task<(bool IsSuccess, int ProviderId)> TryResolve(T info, CancellationToken? ct = null); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/jellyfin-web-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | JELLYFIN_WEB_URL=$(curl -sSL https://api.github.com/repos/jellyfin/jellyfin-web/releases | grep -E 'tarball_url.*jellyfin-web.*$' | head -n 1 | cut -d '"' -f 4) 4 | mkdir /tmp/jellyfin-web 5 | curl -sSL $JELLYFIN_WEB_URL | tar xzC /tmp/jellyfin-web 6 | mkdir /jellyfin-web 7 | mv /tmp/jellyfin-web/jellyfin-*/{,.[^.]}* /jellyfin-web 8 | rm -rf /tmp/jellyfin-web 9 | cd /jellyfin-web 10 | npm install 11 | npm run build:development 12 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/README.md: -------------------------------------------------------------------------------- 1 | # Warning: Folder contents may be replaced 2 | 3 | The contents of this folder will be automatically replaced with a file of the same name in the [vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers) repository's [script-library folder](https://github.com/microsoft/vscode-dev-containers/tree/master/script-library) whenever the repository is packaged. 4 | 5 | To retain your edits, move the file to a different location. You may also delete the files if they are not needed. -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk.Tests/TransliterationTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Jellyfin.Plugin.Kinopoisk.Tests 4 | { 5 | public class TransliterationTests 6 | { 7 | [Theory] 8 | [InlineData("Илья Куликов", "Ilya Kulikov")] 9 | [InlineData("Ирина Старшенбаум", "Irina Starshenbaum")] 10 | public void ShouldTransliterateToLatin(string text, string expected) 11 | { 12 | Assert.Equal(expected, text.TransliterateToLatin()); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/src" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/pull.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request Check 2 | 3 | on: 4 | pull_request: 5 | 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup .NET 8.0 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: 8.0.x 20 | 21 | - name: Build project 22 | run: dotnet build --configuration Release ./src 23 | 24 | - name: Test project 25 | run: dotnet test --configuration Release ./src 26 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using MediaBrowser.Controller.Providers; 4 | using MediaBrowser.Model.Entities; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Jellyfin.Plugin.Kinopoisk 8 | { 9 | public static class Constants 10 | { 11 | public const string ProviderId = "kinopoisk"; 12 | public const string ProviderName = "КиноПоиск"; 13 | public const string ProviderDescription = "Информация о фильмах и сериалах с КиноПоиска"; 14 | public const string ProviderMetadataLanguage = "ru"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /kinopoisk.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "../workspace-jellyfin" 8 | } 9 | ], 10 | "settings": { 11 | "dotnet-test-explorer.enableTelemetry": false, 12 | "dotnet-test-explorer.testProjectPath": "${workspaceFolder:workspace}/src/Jellyfin.Plugin.Kinopoisk.sln", 13 | "omnisharp.defaultLaunchSolution": "Jellyfin.Plugin.Kinopoisk.sln", 14 | "omnisharp.enableRoslynAnalyzers": true, 15 | "omnisharp.organizeImportsOnFormat": true, 16 | "csharp.semanticHighlighting.enabled": true, 17 | "editor.formatOnType": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/Model/KinopoiskPersonExternalId.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Controller.Entities; 2 | using MediaBrowser.Controller.Providers; 3 | using MediaBrowser.Model.Entities; 4 | using MediaBrowser.Model.Providers; 5 | 6 | namespace Jellyfin.Plugin.Kinopoisk.Model 7 | { 8 | public class KinopoiskPersonExternalId : IExternalId 9 | { 10 | public string ProviderName => Constants.ProviderName; 11 | 12 | public string Key => Constants.ProviderId; 13 | 14 | public string UrlFormatString => "https://www.kinopoisk.ru/name/{0}"; 15 | 16 | public ExternalIdMediaType? Type => null; 17 | 18 | public bool Supports(IHasProviderIds item) 19 | { 20 | return item is Person; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/IKinopoiskApiClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace KinopoiskUnofficialInfo.ApiClient 6 | { 7 | public interface IKinopoiskApiClient 8 | { 9 | Task GetPerson(int personId, CancellationToken? cancellationToken = null); 10 | Task GetSingleFilm(int filmId, CancellationToken? cancellationToken = null); 11 | Task> GetStaff(int filmId, CancellationToken? cancellationToken = null); 12 | Task GetTrailers(int filmId, CancellationToken? cancellationToken = null); 13 | Task SearchByKeyword(string keyword, int page = 1, CancellationToken? cancellationToken = null); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk.Tests/Jellyfin.Plugin.Kinopoisk.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/Model/KinopoiskExternalId.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Controller.Entities; 2 | using MediaBrowser.Controller.Entities.Movies; 3 | using MediaBrowser.Controller.Entities.TV; 4 | using MediaBrowser.Controller.Providers; 5 | using MediaBrowser.Model.Entities; 6 | using MediaBrowser.Model.Providers; 7 | 8 | namespace Jellyfin.Plugin.Kinopoisk.Model 9 | { 10 | public class KinopoiskExternalId : IExternalId 11 | { 12 | public string ProviderName => Constants.ProviderName; 13 | 14 | public string Key => Constants.ProviderId; 15 | 16 | public string UrlFormatString => "https://www.kinopoisk.ru/film/{0}"; 17 | 18 | public ExternalIdMediaType? Type => null; 19 | 20 | public bool Supports(IHasProviderIds item) 21 | { 22 | return item is Movie || item is Series; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/Patchers/BadProductionStatusConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | 5 | namespace KinopoiskUnofficialInfo.ApiClient 6 | { 7 | public class BadProductionStatusConverter : StringEnumConverter 8 | { 9 | public override bool CanConvert(System.Type objectType) 10 | => objectType == typeof(FilmProductionStatus); 11 | 12 | public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer) 13 | { 14 | try 15 | { 16 | return base.ReadJson(reader, objectType, existingValue, serializer); 17 | } 18 | catch (Exception) 19 | { 20 | return FilmProductionStatus.UNKNOWN; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/Patchers/BadProfessionKeyConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | 5 | namespace KinopoiskUnofficialInfo.ApiClient 6 | { 7 | public class BadProfessionKeyConverter : StringEnumConverter 8 | { 9 | public override bool CanConvert(System.Type objectType) 10 | => objectType == typeof(PersonResponse_filmsProfessionKey); 11 | 12 | public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer) 13 | { 14 | try 15 | { 16 | return base.ReadJson(reader, objectType, existingValue, serializer); 17 | } 18 | catch (Exception) 19 | { 20 | return PersonResponse_filmsProfessionKey.UNKNOWN; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/Patchers/JsonTransformator.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace KinopoiskUnofficialInfo.ApiClient 4 | { 5 | internal static class JsonTransformator 6 | { 7 | public static JsonSerializerSettings TransformSettings( 8 | JsonSerializerSettings settings) 9 | { 10 | // Опубликованный openapi.json малость кривоват, многое помечено required, но возвращается null 11 | // Либа генерации прокси - тоже малость кривовата, т.к. для string считает явно указанный null 12 | // недопустимым, независимо от required для данного поля. 13 | // С задекларированным составом перечислений тоже беда 14 | // В общем, приходится патчить контракты на лету. 15 | settings.ContractResolver = new DeclarationPatcherContractResolver(); 16 | 17 | return settings; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/Patchers/BadFilmSearchResponse_filmsTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | 5 | namespace KinopoiskUnofficialInfo.ApiClient 6 | { 7 | public class BadFilmSearchResponse_filmsTypeConverter : StringEnumConverter 8 | { 9 | public override bool CanConvert(System.Type objectType) 10 | => objectType == typeof(FilmSearchResponse_filmsType); 11 | 12 | public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer) 13 | { 14 | try 15 | { 16 | return base.ReadJson(reader, objectType, existingValue, serializer); 17 | } 18 | catch (Exception) 19 | { 20 | return FilmSearchResponse_filmsType.UNKNOWN; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/build.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "КиноПоиск" 3 | guid: "0c136f8a-ff77-4f2b-ade5-13462cae6216" 4 | imageUrl: "https://kinopoisk.userecho.com/s/attachments/28876/0/1/25f8c0315e6ccb2aa6c2642e48f2c9e9.png" 5 | version: "10.9.0.1" 6 | targetAbi: "10.9.0" 7 | framework: "net8.0" 8 | owner: "svk" 9 | overview: "Информация о фильмах и сериалах с КиноПоиска" 10 | description: > 11 | Загружает рейтинг, описания, актёров, трейлеры и т.д. с сайта КиноПоиск. 12 | Может потребоваться зарегистрировать свой ApiToken, см. информацию в параметрах плагина. 13 | Для точного распознавания рекомендуется указывать id фильма с сайта КиноПоиск в имени файла в формате kp-12345 или kp12345. 14 | Подробнее см. https://github.com/LinFor/jellyfin-plugin-kinopoisk/blob/master/README.md 15 | category: "Metadata" 16 | artifacts: 17 | - "Jellyfin.Plugin.Kinopoisk.dll" 18 | - "KinopoiskUnofficialInfo.ApiClient.dll" 19 | changelog: > 20 | Fix bugs 21 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/MetadataProviders/MovieMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers; 3 | using KinopoiskUnofficialInfo.ApiClient; 4 | using MediaBrowser.Controller.Entities.Movies; 5 | using MediaBrowser.Controller.Providers; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Jellyfin.Plugin.Kinopoisk.MetadataProviders 9 | { 10 | public class MovieMetadataProvider : BaseVideoMetadataProvider 11 | { 12 | public MovieMetadataProvider(IKinopoiskApiClient kinopoiskApiClient, IProviderIdResolver providerIdResolver, ILogger logger, IHttpClientFactory httpClientFactory) 13 | : base(kinopoiskApiClient, providerIdResolver, logger, httpClientFactory) 14 | { 15 | } 16 | 17 | protected override Movie ConvertResponseToItem(Film apiResponse) 18 | => apiResponse.ToMovie(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/MetadataProviders/SeriesMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers; 3 | using KinopoiskUnofficialInfo.ApiClient; 4 | using MediaBrowser.Controller.Entities.TV; 5 | using MediaBrowser.Controller.Providers; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Jellyfin.Plugin.Kinopoisk.MetadataProviders 9 | { 10 | public class SeriesMetadataProvider : BaseVideoMetadataProvider 11 | { 12 | public SeriesMetadataProvider(IKinopoiskApiClient kinopoiskApiClient, IProviderIdResolver providerIdResolver, ILogger logger, IHttpClientFactory httpClientFactory) 13 | : base(kinopoiskApiClient, providerIdResolver, logger, httpClientFactory) 14 | { 15 | } 16 | 17 | protected override Series ConvertResponseToItem(Film apiResponse) 18 | => apiResponse.ToSeries(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/Patchers/BadIntegerConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace KinopoiskUnofficialInfo.ApiClient 5 | { 6 | public class BadIntegerConverter : JsonConverter 7 | { 8 | public override bool CanRead => true; 9 | public override bool CanWrite => false; 10 | public override bool CanConvert(System.Type objectType) 11 | => objectType == typeof(Int32); 12 | 13 | public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer) 14 | { 15 | if (int.TryParse(Convert.ToString(reader.Value), out var result)) 16 | return result; 17 | 18 | return -1; 19 | } 20 | 21 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 22 | { 23 | throw new NotImplementedException(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/Patchers/BadDoubleConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace KinopoiskUnofficialInfo.ApiClient 5 | { 6 | public class BadDoubleConverter : JsonConverter 7 | { 8 | public override bool CanRead => true; 9 | public override bool CanWrite => false; 10 | public override bool CanConvert(System.Type objectType) 11 | => objectType == typeof(double); 12 | 13 | public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer) 14 | { 15 | if (double.TryParse(Convert.ToString(reader.Value), out var result)) 16 | return result; 17 | 18 | return 0.0d; 19 | } 20 | 21 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 22 | { 23 | throw new NotImplementedException(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/Jellyfin.Plugin.Kinopoisk.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | portable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/MetadataProviders/BaseMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using MediaBrowser.Common.Net; 5 | using MediaBrowser.Controller.Providers; 6 | 7 | namespace Jellyfin.Plugin.Kinopoisk.MetadataProviders 8 | { 9 | public abstract class BaseMetadataProvider : IMetadataProvider 10 | { 11 | protected IHttpClientFactory _httpClientFactory; 12 | 13 | protected BaseMetadataProvider(IHttpClientFactory httpClientFactory) 14 | { 15 | _httpClientFactory = httpClientFactory ?? throw new System.ArgumentNullException(nameof(httpClientFactory)); 16 | } 17 | 18 | public string Name => Constants.ProviderName; 19 | 20 | public async Task GetImageResponse(string url, CancellationToken cancellationToken) 21 | { 22 | return await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetTrailers_ShouldReturnEmptyResponse_948870.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v2.2/films/948870/videos 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:32 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | {"total":0,"items":[]} 47 | httpVersion: 1.1 48 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/KinopoiskUnofficialInfo.ApiClient.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/ProviderIdResolvers/CommonResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using MediaBrowser.Model.Entities; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers 7 | { 8 | public class CommonResolver : IProviderIdResolver 9 | where T : IHasProviderIds 10 | { 11 | protected readonly ILogger> _logger; 12 | 13 | public CommonResolver(ILogger> logger) 14 | { 15 | _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); 16 | } 17 | 18 | public virtual Task<(bool IsSuccess, int ProviderId)> TryResolve(T info, CancellationToken? ct = null) 19 | { 20 | // Try to get from stored metadata 21 | var kinopoiskIdStr = info.GetProviderId(Constants.ProviderId); 22 | 23 | // Try to get from stored metadata 24 | if (!string.IsNullOrEmpty(kinopoiskIdStr) && int.TryParse(kinopoiskIdStr, out var result)) 25 | { 26 | _logger.LogDebug($"Got KinopoiskProviderId from metadata ({result})"); 27 | return Task.FromResult((true, result)); 28 | } 29 | 30 | return Task.FromResult((false, 0)); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] .NET version: 5.0, 3.1, 2.1 2 | ARG VARIANT=3.1 3 | FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT} 4 | 5 | # [Option] Install Node.js 6 | ARG INSTALL_NODE="true" 7 | ARG NODE_VERSION="lts/*" 8 | RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 9 | 10 | # [Option] Install Azure CLI 11 | ARG INSTALL_AZURE_CLI="false" 12 | COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ 13 | RUN if [ "$INSTALL_AZURE_CLI" = "true" ]; then bash /tmp/library-scripts/azcli-debian.sh; fi \ 14 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 15 | 16 | # [Optional] Uncomment this section to install additional OS packages. 17 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 18 | && apt-get -y install --no-install-recommends \ 19 | python3 \ 20 | pip \ 21 | && pip install jprm \ 22 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* 23 | 24 | # [Optional] Uncomment this line to install global node packages. 25 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 26 | 27 | COPY library-scripts/jellyfin-web-install.sh /tmp/library-scripts/ 28 | RUN /tmp/library-scripts/jellyfin-web-install.sh && rm -rf /tmp/library-scripts 29 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Jellyfin.Plugin.Kinopoisk.Configuration; 4 | using MediaBrowser.Common.Configuration; 5 | using MediaBrowser.Common.Plugins; 6 | using MediaBrowser.Model.Plugins; 7 | using MediaBrowser.Model.Serialization; 8 | 9 | namespace Jellyfin.Plugin.Kinopoisk 10 | { 11 | public class Plugin : BasePlugin, IHasWebPages 12 | { 13 | public static Plugin Instance { get; private set; } 14 | 15 | public override string Name => Constants.ProviderName; 16 | 17 | public override string Description => Constants.ProviderDescription; 18 | 19 | public override Guid Id => Guid.Parse("33e6d249-648f-44cd-a9ce-497be06c08df"); 20 | 21 | public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) 22 | { 23 | Instance = this; 24 | } 25 | 26 | public IEnumerable GetPages() 27 | { 28 | return new[] 29 | { 30 | new PluginPageInfo 31 | { 32 | Name = this.Name, 33 | EmbeddedResourcePath = string.Format("{0}.Configuration.configPage.html", GetType().Namespace) 34 | } 35 | }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jellyfin-plugin-kinopoisk 2 | 3 | Fetches metadata from https://www.kinopoisk.ru/. This site is popular in the Russian-speaking community and contains almost no English-language information, so further description will be in Russian. 4 | 5 | ## Установка 6 | 7 | Администрирование - Панель - Расширенное - Плагины - вкладка Репозитории - добавить адрес https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/master/dist/manifest.json. 8 | 9 | После этого на вкладке Каталог найти "КиноПоиск" (раздел Метаданные) и установить. 10 | 11 | ## Настройка 12 | 13 | Параметры плагина искать в: Администрирование - Панель - Расширенное - Плагины - вкладка "Мои плагины" - КиноПоиск - "три точки" - Параметры 14 | 15 | Если плагин не работает или работает плохо - попробуйте зарегистрировать (и указать в параметрах) свой собственный ApiToken (на сайте https://kinopoiskapiunofficial.tech). По-умолчанию прописан общий, ограничение порядка 10 запросов/сек - для общего ApiToken быстро заканчивается. 16 | 17 | ## Использование 18 | 19 | Поддерживаются: 20 | - Фильмы 21 | - Сериалы 22 | 23 | На данный момент грузятся: 24 | - Рейтинг 25 | - Описание 26 | - Постеры и задники 27 | - Актёры 28 | - Трейлеры (только те, что лежат на ютубе - Jellyfin-Web не умеет играть трейлеры, лежащие на самом КиноПоиске) 29 | 30 | Плагин будет пытаться найти в имени файла (для фильмов) или имени корневой папки (для сериалов) паттерн вида "kp-12345" или "kp12345", где число - id фильма на сайте КиноПоиск. 31 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/RemoteImageUrlSanitizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace Jellyfin.Plugin.Kinopoisk 6 | { 7 | public class RemoteImageUrlSanitizer 8 | { 9 | private readonly HttpClient _httpClient; 10 | 11 | public RemoteImageUrlSanitizer(HttpClient httpClient) 12 | { 13 | _httpClient = httpClient ?? throw new System.ArgumentNullException(nameof(httpClient)); 14 | } 15 | 16 | public async Task SanitizeRemoteImageUrl(string url) 17 | { 18 | if (string.IsNullOrEmpty(url)) 19 | return null; 20 | 21 | var currentUrl = url; 22 | while (!currentUrl.Contains("no-poster")) 23 | { 24 | var response = await _httpClient.SendAsync( 25 | new HttpRequestMessage(HttpMethod.Get, currentUrl), HttpCompletionOption.ResponseHeadersRead); 26 | 27 | if ((int)response.StatusCode <= 299) 28 | return currentUrl; 29 | else if (response.Headers.Location != null) 30 | { 31 | currentUrl = response.Headers.Location.ToString(); 32 | continue; 33 | } 34 | else 35 | throw new InvalidOperationException($"Unexpected answer HTTP {response.StatusCode}"); 36 | } 37 | 38 | return null; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/ProviderIdResolvers/CommonLookupInfoResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using MediaBrowser.Controller.Providers; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers 8 | { 9 | public class CommonLookupInfoResolver : CommonResolver 10 | where T : ItemLookupInfo 11 | { 12 | private readonly Regex _kinopoiskIdRegex = new(@"kp-?(?\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); 13 | 14 | public CommonLookupInfoResolver(ILogger> logger) 15 | : base(logger) 16 | { 17 | } 18 | public override async Task<(bool IsSuccess, int ProviderId)> TryResolve(T info, CancellationToken? ct = null) 19 | { 20 | // Try to get from stored metadata 21 | var baseResult = await base.TryResolve(info, ct); 22 | if (baseResult.IsSuccess) 23 | return baseResult; 24 | 25 | // Try to get from filename 26 | if (!string.IsNullOrEmpty(info.Path)) 27 | { 28 | var match = _kinopoiskIdRegex.Match(info.Path); 29 | if (match.Success && int.TryParse(match.Groups["kinopoiskId"].Value, out var result)) 30 | { 31 | _logger.LogDebug($"Got KinopoiskProviderId from filename ({result}, {info.Path})"); 32 | return (true, result); 33 | } 34 | } 35 | 36 | return (false, 0); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/azcli-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/master/script-library/docs/azcli.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./azcli-debian.sh 11 | 12 | set -e 13 | 14 | if [ "$(id -u)" -ne 0 ]; then 15 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 16 | exit 1 17 | fi 18 | 19 | export DEBIAN_FRONTEND=noninteractive 20 | 21 | # Install curl, apt-transport-https, lsb-release, or gpg if missing 22 | if ! dpkg -s apt-transport-https curl ca-certificates lsb-release > /dev/null 2>&1 || ! type gpg > /dev/null 2>&1; then 23 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 24 | apt-get update 25 | fi 26 | apt-get -y install --no-install-recommends apt-transport-https curl ca-certificates lsb-release gnupg2 27 | fi 28 | 29 | # Install the Azure CLI 30 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list 31 | curl -sL https://packages.microsoft.com/keys/microsoft.asc | (OUT=$(apt-key add - 2>&1) || echo $OUT) 32 | apt-get update 33 | apt-get install -y azure-cli 34 | echo "Done!" -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/KinopoiskUnofficialInfo.ApiClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | 18 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "publish-to-run", 12 | "program": "${workspaceFolder:workspace-jellyfin}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder:workspace-jellyfin}/Jellyfin.Server", 15 | "console": "internalConsole", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | // "serverReadyAction": { 19 | // "action": "openExternally", 20 | // "pattern": "Overriding address\\(es\\) \\'(https?:\\S+)\\'", 21 | // } 22 | }, 23 | // { 24 | // "name": ".NET Core Launch (console)", 25 | // "type": "coreclr", 26 | // "request": "launch", 27 | // "preLaunchTask": "build", 28 | // "program": "${workspaceFolder}/src/Jellyfin.Plugin.Kinopoisk/bin/Debug/net5.0/Jellyfin.Plugin.Kinopoisk.dll", 29 | // "args": [], 30 | // "cwd": "${workspaceFolder}/src", 31 | // "console": "internalConsole", 32 | // "stopAtEntry": false 33 | // }, 34 | { 35 | "name": ".NET Core Attach", 36 | "type": "coreclr", 37 | "request": "attach", 38 | "processId": "${command:pickProcess}" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/Patchers/DeclarationPatcherContractResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Serialization; 6 | 7 | namespace KinopoiskUnofficialInfo.ApiClient 8 | { 9 | public class DeclarationPatcherContractResolver : DefaultContractResolver 10 | { 11 | private readonly ICollection _badConverters = new JsonConverter[] { 12 | new BadIntegerConverter(), 13 | new BadDoubleConverter(), 14 | new BadProfessionKeyConverter(), 15 | new BadProductionStatusConverter(), 16 | new BadFilmSearchResponse_filmsTypeConverter(), 17 | }; 18 | 19 | protected override IList CreateProperties(System.Type type, MemberSerialization memberSerialization) 20 | { 21 | var list = base.CreateProperties(type, memberSerialization); 22 | foreach (var property in list) 23 | { 24 | foreach (var badConverter in _badConverters) 25 | { 26 | if (badConverter.CanConvert(property.PropertyType)) 27 | { 28 | #pragma warning disable CS0618 29 | property.MemberConverter = null; 30 | #pragma warning restore CS0618 31 | property.Converter = badConverter; 32 | 33 | property.Required = Required.Default; 34 | // property.DefaultValue = -1; 35 | break; 36 | } 37 | } 38 | 39 | if (property.PropertyType == typeof(string)) 40 | { 41 | property.Required = Required.Default; 42 | } 43 | } 44 | return list; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.devcontainer/base.Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] .NET version: 5.0, 3.1, 2.1 2 | ARG VARIANT="6.0" 3 | FROM mcr.microsoft.com/dotnet/sdk:${VARIANT}-focal 4 | 5 | # Copy library scripts to execute 6 | COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ 7 | 8 | # [Option] Install zsh 9 | ARG INSTALL_ZSH="true" 10 | # [Option] Upgrade OS packages to their latest versions 11 | ARG UPGRADE_PACKAGES="true" 12 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 13 | ARG USERNAME=vscode 14 | ARG USER_UID=1000 15 | ARG USER_GID=$USER_UID 16 | RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ 17 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* 18 | 19 | # [Option] Install Node.js 20 | ARG INSTALL_NODE="true" 21 | ARG NODE_VERSION="none" 22 | ENV NVM_DIR=/usr/local/share/nvm 23 | ENV NVM_SYMLINK_CURRENT=true \ 24 | PATH=${NVM_DIR}/current/bin:${PATH} 25 | RUN if [ "$INSTALL_NODE" = "true" ]; then bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}"; fi \ 26 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* 27 | 28 | # [Option] Install Azure CLI 29 | ARG INSTALL_AZURE_CLI="false" 30 | RUN if [ "$INSTALL_AZURE_CLI" = "true" ]; then bash /tmp/library-scripts/azcli-debian.sh; fi \ 31 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* 32 | 33 | # Remove library scripts for final image 34 | RUN rm -rf /tmp/library-scripts 35 | 36 | # [Optional] Uncomment this section to install additional OS packages. 37 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 38 | # && apt-get -y install --no-install-recommends 39 | 40 | # [Optional] Uncomment this line to install global node packages. 41 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 42 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/KinopoiskPluginServiceRegistrator.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers; 3 | using KinopoiskUnofficialInfo.ApiClient; 4 | using MediaBrowser.Controller; 5 | using MediaBrowser.Controller.Plugins; 6 | using MediaBrowser.Controller.Entities; 7 | using MediaBrowser.Controller.Providers; 8 | using Microsoft.Extensions.Caching.Memory; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace Jellyfin.Plugin.Kinopoisk 13 | { 14 | /// 15 | /// Registers services 16 | /// 17 | public class KinopoiskPluginServiceRegistrator : IPluginServiceRegistrator 18 | { 19 | public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost) 20 | { 21 | serviceCollection.AddSingleton((sp) => new KinopoiskApiClient( 22 | Plugin.Instance.Configuration.ApiToken, 23 | sp.GetRequiredService>(), 24 | sp.GetRequiredService() 25 | )); 26 | serviceCollection.AddSingleton((sp) => new CachedKinopoiskApiClient( 27 | sp.GetRequiredService(), 28 | sp.GetRequiredService(), 29 | sp.GetRequiredService>() 30 | )); 31 | 32 | serviceCollection.AddSingleton, VideoResolver>(); 33 | serviceCollection.AddSingleton, VideoResolver>(); 34 | serviceCollection.AddSingleton, CommonResolver>(); 35 | serviceCollection.AddSingleton, CommonResolver>(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetTrailers_ShouldParseName_1395460.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v2.2/films/1395460/videos 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:31 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | {"total":6,"items":[{"url":"https://youtu.be/CoHR6BiI4TA","name":"О съёмках (сезон 1)","site":"UNKNOWN"},{"url":"https://youtu.be/GAXMdM2gkZ0","name":"Трейлер (сезон 1)","site":"UNKNOWN"},{"url":"https://trailers.s3.mds.yandex.net/video_original/173240-7271863259028188.mp4","name":"Фрагмент (сезон 1)","site":"UNKNOWN"},{"url":"https://trailers.s3.mds.yandex.net/video_original/173714-4075129810108049.mp4","name":"Трейлер №2 (сезон 1)","site":"UNKNOWN"},{"url":"https://trailers.s3.mds.yandex.net/video_original/174413-3555489888487551.mp4","name":"Интернет-трейлер (сезон 1)","site":"UNKNOWN"},{"url":"https://youtu.be/83fRcdyrVg0","name":"Трейлер (сезон 2)","site":"UNKNOWN"}]} 47 | httpVersion: 1.1 48 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | //devcontainer.json 2 | { 3 | "name": "Kinopoisk plugin (jellyfin) Devcontainer", 4 | "build": { 5 | "dockerfile": "Dockerfile", 6 | "args": { 7 | // Update 'VARIANT' to pick a .NET Core version: 2.1, 3.1, 5.0 8 | "VARIANT": "5.0", 9 | // Options 10 | "INSTALL_NODE": "true", 11 | "NODE_VERSION": "lts/*", 12 | "INSTALL_AZURE_CLI": "false" 13 | } 14 | }, 15 | "workspaceFolder": "/workspace", 16 | "workspaceMount": "source=/home/pingwin/development/jellyfin-kinopoisk,target=/workspace,type=bind,consistency=cached", 17 | "extensions": [ 18 | "ms-dotnettools.csharp", 19 | "mhutchie.git-graph", 20 | "github.vscode-pull-request-github", 21 | "editorconfig.editorconfig", 22 | "formulahendry.dotnet-test-explorer" 23 | ], 24 | "forwardPorts": [ 25 | 8096, 26 | 8920, 27 | 1900, 28 | 7359 29 | ], 30 | // "postCreateCommand": "apt-get update && apt-get install -y git", 31 | // "runArgs": [ 32 | // "--datadir=/config", 33 | // "--cachedir=/cache", 34 | // "--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg" 35 | // ], 36 | "mounts": [ 37 | "source=vscode-extensions,target=/root/.vscode-server/extensions,type=volume", 38 | "source=/home/pingwin/development/jellyfin-data,target=/jellyfin-data,type=bind,consistency=cached", 39 | "source=/storage/torrents,target=/jellyfin-media,type=bind,consistency=cached", 40 | "source=/home/pingwin/development/jellyfin,target=/workspace-jellyfin,type=bind,consistency=cached", 41 | ], 42 | "containerEnv": { 43 | "DOTNET_CLI_TELEMETRY_OPTOUT": "1", 44 | "DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE": "false", 45 | "JELLYFIN_DATA_DIR": "/jellyfin-data", 46 | "JELLYFIN_WEB_DIR": "/jellyfin-web/dist", 47 | }, 48 | "portsAttributes": { 49 | "1900": { 50 | "label": "ssdp" 51 | }, 52 | "7359": { 53 | "label": "discovery" 54 | }, 55 | "8096": { 56 | "label": "www" 57 | }, 58 | "8920": { 59 | "label": "www-ssl" 60 | } 61 | }, 62 | // "runArgs": [ 63 | // "--env-file", 64 | // ".devcontainer/devcontainer.env" 65 | // ], 66 | } 67 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/RemoteImageProviders/BaseImageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using MediaBrowser.Common.Net; 8 | using MediaBrowser.Controller.Entities; 9 | using MediaBrowser.Controller.Providers; 10 | using MediaBrowser.Model.Entities; 11 | using MediaBrowser.Model.Providers; 12 | 13 | namespace Jellyfin.Plugin.Kinopoisk.MetadataProviders 14 | { 15 | public abstract class BaseImageProvider : IRemoteImageProvider 16 | { 17 | private readonly IHttpClientFactory _httpClientFactory; 18 | 19 | protected BaseImageProvider(IHttpClientFactory httpClientFactory) 20 | { 21 | _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); 22 | } 23 | 24 | public abstract string Name { get; } 25 | 26 | public abstract bool Supports(BaseItem item); 27 | 28 | public async Task GetImageResponse(string url, CancellationToken cancellationToken) 29 | { 30 | return await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); 31 | } 32 | 33 | public abstract Task> GetImages(BaseItem item, CancellationToken cancellationToken); 34 | 35 | public abstract IEnumerable GetSupportedImages(BaseItem item); 36 | 37 | protected async Task> FilterEmptyImages(IEnumerable images) 38 | { 39 | using var httpClient = new HttpClient(new HttpClientHandler() { AllowAutoRedirect = false }, true); 40 | var sanitizer = new RemoteImageUrlSanitizer(httpClient); 41 | var res = await Task.WhenAll(images.Select(async i => { 42 | var sanitizedUrl = await sanitizer.SanitizeRemoteImageUrl(i.Url); 43 | if (string.IsNullOrEmpty(sanitizedUrl)) 44 | return null; 45 | 46 | i.Url = sanitizedUrl; 47 | return i; 48 | })); 49 | 50 | return res.Where(i => i != null).ToArray(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/RemoteImageProviders/PersonImageProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers; 7 | using KinopoiskUnofficialInfo.ApiClient; 8 | using MediaBrowser.Controller.Entities; 9 | using MediaBrowser.Model.Entities; 10 | using MediaBrowser.Model.Providers; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace Jellyfin.Plugin.Kinopoisk.MetadataProviders 14 | { 15 | public class PersonImageProvider : BaseImageProvider 16 | { 17 | private readonly IKinopoiskApiClient _apiClient; 18 | private readonly IProviderIdResolver _providerIdResolver; 19 | private readonly ILogger _logger; 20 | 21 | public PersonImageProvider(IKinopoiskApiClient kinopoiskApiClient, IProviderIdResolver providerIdResolver, ILogger logger, IHttpClientFactory httpClientFactory) 22 | : base(httpClientFactory) 23 | { 24 | _apiClient = kinopoiskApiClient ?? throw new System.ArgumentNullException(nameof(kinopoiskApiClient)); 25 | _providerIdResolver = providerIdResolver ?? throw new System.ArgumentNullException(nameof(providerIdResolver)); 26 | _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); 27 | } 28 | 29 | public override string Name => Constants.ProviderName; 30 | 31 | public override bool Supports(BaseItem item) 32 | => item is Person; 33 | 34 | public override async Task> GetImages(BaseItem item, CancellationToken cancellationToken) 35 | { 36 | var (resolveResult, kinopoiskId) = await _providerIdResolver.TryResolve(item, cancellationToken); 37 | if (!resolveResult) 38 | return Enumerable.Empty(); 39 | 40 | var person = await _apiClient.GetPerson(kinopoiskId, cancellationToken); 41 | 42 | var res = new[] { person.ToRemoteImageInfo() }; 43 | return await FilterEmptyImages(res); 44 | } 45 | 46 | public override IEnumerable GetSupportedImages(BaseItem item) 47 | { 48 | yield return ImageType.Primary; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/RemoteImageProviders/VideoImageProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers; 8 | using KinopoiskUnofficialInfo.ApiClient; 9 | using MediaBrowser.Controller.Entities; 10 | using MediaBrowser.Controller.Entities.Movies; 11 | using MediaBrowser.Controller.Entities.TV; 12 | using MediaBrowser.Model.Entities; 13 | using MediaBrowser.Model.Providers; 14 | using Microsoft.Extensions.Logging; 15 | 16 | namespace Jellyfin.Plugin.Kinopoisk.MetadataProviders 17 | { 18 | public class VideoImageProvider : BaseImageProvider 19 | { 20 | private readonly ILogger _logger; 21 | private readonly IKinopoiskApiClient _apiClient; 22 | private readonly IProviderIdResolver _providerIdResolver; 23 | 24 | public override string Name => Constants.ProviderName; 25 | 26 | public VideoImageProvider(IKinopoiskApiClient kinopoiskApiClient, IProviderIdResolver providerIdResolver, ILogger logger, IHttpClientFactory httpClientFactory) 27 | : base(httpClientFactory) 28 | { 29 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 30 | _apiClient = kinopoiskApiClient ?? throw new ArgumentNullException(nameof(kinopoiskApiClient)); 31 | _providerIdResolver = providerIdResolver ?? throw new ArgumentNullException(nameof(providerIdResolver)); 32 | } 33 | 34 | public override bool Supports(BaseItem item) 35 | => item is Movie || item is Series; 36 | 37 | public override IEnumerable GetSupportedImages(BaseItem item) 38 | => new ImageType[] 39 | { 40 | ImageType.Primary, 41 | ImageType.Backdrop 42 | }; 43 | 44 | public override async Task> GetImages(BaseItem item, CancellationToken cancellationToken) 45 | { 46 | var (resolveResult, kinopoiskId) = await _providerIdResolver.TryResolve(item, cancellationToken); 47 | if (!resolveResult) 48 | return Enumerable.Empty(); 49 | 50 | var film = await _apiClient.GetSingleFilm(kinopoiskId, cancellationToken); 51 | 52 | return await FilterEmptyImages(film.ToRemoteImageInfos()); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/Configuration/configPage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | КиноПоиск 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 | 14 | 15 |
Personal api token, you can get your own on 16 | https://kinopoiskapiunofficial.tech 17 |
18 |
19 |
20 | 23 |
24 |
25 |
26 |
27 | 51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetSingleFilm_ShouldParseFilm_1445243.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v2.2/films/1445243 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:32 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | {"kinopoiskId":1445243,"imdbId":"tt14362510","nameRu":"Будь моим Кириллом","nameEn":null,"nameOriginal":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/posters/kp/1445243.jpg","posterUrlPreview":"https://kinopoiskapiunofficial.tech/images/posters/kp_small/1445243.jpg","reviewsCount":5,"ratingGoodReview":100.0,"ratingGoodReviewVoteCount":5,"ratingKinopoisk":6.4,"ratingKinopoiskVoteCount":3495,"ratingImdb":6.2,"ratingImdbVoteCount":51,"ratingFilmCritics":null,"ratingFilmCriticsVoteCount":0,"ratingAwait":null,"ratingAwaitCount":383,"ratingRfCritics":null,"ratingRfCriticsVoteCount":1,"webUrl":"https://www.kinopoisk.ru/film/1445243/","year":2021,"filmLength":96,"slogan":null,"description":"Неуверенная в себе девушка Саша врет семье, что встречается с красавчиком Кириллом, хотя он — всего лишь ее тренер по бегу, с которым она боится даже заговорить. Муж сестры Паша знает правду, но у него и своих проблем хватает — у них с женой уже год не было секса. Записавшись вместе на беготерапию, Саша и Паша решают, что смогут убежать от проблем вместе.","shortDescription":null,"editorAnnotation":null,"isTicketsAvailable":false,"productionStatus":null,"type":"FILM","ratingMpaa":null,"ratingAgeLimits":"age16","countries":[{"country":"Россия"}],"genres":[{"genre":"комедия"}],"startYear":null,"endYear":null,"serial":false,"shortFilm":false,"completed":false,"hasImax":false,"has3D":false,"lastSync":"2021-09-05T18:38:10.752761"} 47 | httpVersion: 1.1 48 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/MetadataProviders/PersonMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers; 7 | using KinopoiskUnofficialInfo.ApiClient; 8 | using MediaBrowser.Controller.Entities; 9 | using MediaBrowser.Controller.Providers; 10 | using MediaBrowser.Model.Providers; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace Jellyfin.Plugin.Kinopoisk.MetadataProviders 14 | { 15 | public class PersonMetadataProvider : BaseMetadataProvider, IRemoteMetadataProvider 16 | { 17 | private readonly IKinopoiskApiClient _apiClient; 18 | private readonly IProviderIdResolver _providerIdResolver; 19 | private readonly ILogger _logger; 20 | 21 | public PersonMetadataProvider(IKinopoiskApiClient apiClient, IProviderIdResolver providerIdResolver, ILogger logger, IHttpClientFactory httpClientFactory) 22 | : base(httpClientFactory) 23 | { 24 | _apiClient = apiClient ?? throw new System.ArgumentNullException(nameof(apiClient)); 25 | _providerIdResolver = providerIdResolver ?? throw new System.ArgumentNullException(nameof(providerIdResolver)); 26 | _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); 27 | } 28 | 29 | public async Task> GetMetadata(PersonLookupInfo info, CancellationToken cancellationToken) 30 | { 31 | var result = new MetadataResult() 32 | { 33 | QueriedById = true, 34 | Provider = Constants.ProviderName, 35 | ResultLanguage = Constants.ProviderMetadataLanguage 36 | }; 37 | 38 | var (resolveResult, kinopoiskId) = await _providerIdResolver.TryResolve(info, cancellationToken); 39 | if (!resolveResult) 40 | return result; 41 | 42 | var person = await _apiClient.GetPerson(kinopoiskId, cancellationToken); 43 | 44 | cancellationToken.ThrowIfCancellationRequested(); 45 | 46 | result.Item = person.ToPerson(); 47 | if (result.Item != null) 48 | result.HasMetadata = true; 49 | 50 | return result; 51 | } 52 | 53 | public Task> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) 54 | => Task.FromResult(Enumerable.Empty()); // Not supported 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetSingleFilm_ShouldParseTvShow_4416198.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v2.2/films/4416198 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:31 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | {"kinopoiskId":4416198,"imdbId":"tt14445576","nameRu":"В активном поиске","nameEn":null,"nameOriginal":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/posters/kp/4416198.jpg","posterUrlPreview":"https://kinopoiskapiunofficial.tech/images/posters/kp_small/4416198.jpg","reviewsCount":3,"ratingGoodReview":100.0,"ratingGoodReviewVoteCount":3,"ratingKinopoisk":5.8,"ratingKinopoiskVoteCount":1570,"ratingImdb":null,"ratingImdbVoteCount":0,"ratingFilmCritics":null,"ratingFilmCriticsVoteCount":0,"ratingAwait":null,"ratingAwaitCount":8,"ratingRfCritics":null,"ratingRfCriticsVoteCount":0,"webUrl":"https://www.kinopoisk.ru/series/4416198/","year":2021,"filmLength":15,"slogan":null,"description":"В успешной жизни соучредителя крупного рекламного агентства Андрея есть всё: и квартира в центре города, и модные друзья, и большое будущее в рекламной сфере. Но однажды на пороге его дома появляется девятилетний ребенок, утверждающий, что он его сын. Теперь Андрею предстоит нелегкий путь: стать отцом, не отказываясь от карьеры, и, главное, найти мать ребенка из десятков своих бывших возлюбленных.","shortDescription":null,"editorAnnotation":null,"isTicketsAvailable":false,"productionStatus":null,"type":"TV_SERIES","ratingMpaa":null,"ratingAgeLimits":"age16","countries":[{"country":"Россия"}],"genres":[{"genre":"комедия"}],"startYear":2021,"endYear":2021,"serial":true,"shortFilm":false,"completed":false,"hasImax":false,"has3D":false,"lastSync":"2021-09-24T07:37:28.311332"} 47 | httpVersion: 1.1 48 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetSingleFilm_ShouldParseFilm_251733.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v2.2/films/251733 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:32 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | {"kinopoiskId":251733,"imdbId":"tt0499549","nameRu":"Аватар","nameEn":null,"nameOriginal":"Avatar","posterUrl":"https://kinopoiskapiunofficial.tech/images/posters/kp/251733.jpg","posterUrlPreview":"https://kinopoiskapiunofficial.tech/images/posters/kp_small/251733.jpg","reviewsCount":2425,"ratingGoodReview":75.6,"ratingGoodReviewVoteCount":1666,"ratingKinopoisk":7.9,"ratingKinopoiskVoteCount":694711,"ratingImdb":7.8,"ratingImdbVoteCount":1155159,"ratingFilmCritics":7.4,"ratingFilmCriticsVoteCount":322,"ratingAwait":85.93,"ratingAwaitCount":38699,"ratingRfCritics":75.0,"ratingRfCriticsVoteCount":12,"webUrl":"https://www.kinopoisk.ru/film/251733/","year":2009,"filmLength":162,"slogan":"Это новый мир","description":"Бывший морпех Джейк Салли прикован к инвалидному креслу. Несмотря на немощное тело, Джейк в душе по-прежнему остается воином. Он получает задание совершить путешествие в несколько световых лет к базе землян на планете Пандора, где корпорации добывают редкий минерал, имеющий огромное значение для выхода Земли из энергетического кризиса.","shortDescription":"Парализованный морпех становится мессией жителей Пандоры. Уникальный мир — масштабный проект Джеймса Кэмерона","editorAnnotation":null,"isTicketsAvailable":false,"productionStatus":null,"type":"FILM","ratingMpaa":"pg13","ratingAgeLimits":"age12","countries":[{"country":"США"},{"country":"Великобритания"}],"genres":[{"genre":"драма"},{"genre":"фантастика"},{"genre":"приключения"},{"genre":"боевик"}],"startYear":null,"endYear":null,"serial":false,"shortFilm":false,"completed":false,"hasImax":true,"has3D":true,"lastSync":"2021-09-19T02:13:38.396756"} 47 | httpVersion: 1.1 48 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.6.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Kinopoisk", "Jellyfin.Plugin.Kinopoisk\Jellyfin.Plugin.Kinopoisk.csproj", "{92F27832-191C-414F-A50D-5098D5C5C35C}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Kinopoisk.Tests", "Jellyfin.Plugin.Kinopoisk.Tests\Jellyfin.Plugin.Kinopoisk.Tests.csproj", "{440F7DCA-3815-4B72-8984-8630250801A5}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KinopoiskUnofficialInfo.ApiClient", "KinopoiskUnofficialInfo.ApiClient\KinopoiskUnofficialInfo.ApiClient.csproj", "{9584F66B-8EAB-4CA8-AC55-E9503EA23CC9}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KinopoiskUnofficialInfo.ApiClient.Tests", "KinopoiskUnofficialInfo.ApiClient.Tests\KinopoiskUnofficialInfo.ApiClient.Tests.csproj", "{0AB018D0-655E-48DF-B414-22EA61C926C9}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {92F27832-191C-414F-A50D-5098D5C5C35C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {92F27832-191C-414F-A50D-5098D5C5C35C}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {92F27832-191C-414F-A50D-5098D5C5C35C}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {92F27832-191C-414F-A50D-5098D5C5C35C}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {440F7DCA-3815-4B72-8984-8630250801A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {440F7DCA-3815-4B72-8984-8630250801A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {440F7DCA-3815-4B72-8984-8630250801A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {440F7DCA-3815-4B72-8984-8630250801A5}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {9584F66B-8EAB-4CA8-AC55-E9503EA23CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {9584F66B-8EAB-4CA8-AC55-E9503EA23CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {9584F66B-8EAB-4CA8-AC55-E9503EA23CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {9584F66B-8EAB-4CA8-AC55-E9503EA23CC9}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {0AB018D0-655E-48DF-B414-22EA61C926C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {0AB018D0-655E-48DF-B414-22EA61C926C9}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {0AB018D0-655E-48DF-B414-22EA61C926C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {0AB018D0-655E-48DF-B414-22EA61C926C9}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetSingleFilm_ShouldParseFilm_1044982.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v2.2/films/1044982 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:32 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | {"kinopoiskId":1044982,"imdbId":"tt6663582","nameRu":"Шпион, который меня кинул","nameEn":null,"nameOriginal":"The Spy Who Dumped Me","posterUrl":"https://kinopoiskapiunofficial.tech/images/posters/kp/1044982.jpg","posterUrlPreview":"https://kinopoiskapiunofficial.tech/images/posters/kp_small/1044982.jpg","reviewsCount":14,"ratingGoodReview":67.9,"ratingGoodReviewVoteCount":8,"ratingKinopoisk":5.9,"ratingKinopoiskVoteCount":48390,"ratingImdb":6.1,"ratingImdbVoteCount":73219,"ratingFilmCritics":5.3,"ratingFilmCriticsVoteCount":208,"ratingAwait":92.52,"ratingAwaitCount":3086,"ratingRfCritics":16.6667,"ratingRfCriticsVoteCount":6,"webUrl":"https://www.kinopoisk.ru/film/1044982/","year":2018,"filmLength":117,"slogan":"Минимум опыта. Максимум последствий","description":"Одри и Морган, лучшие подруги из Лос-Анджелеса, неожиданно оказываются в эпицентре международного заговора, когда бывший Одри заявляется к ней с толпой идущих по следу кровожадных убийц. Удивляясь сами себе, девушки берутся за нелегкое дело спасения мира. Их ждет полная опасностей – и киллеров – шпионская гонка по всей Европе, где на пути им встречается очень подозрительный, но такой обаятельный британский агент...","shortDescription":"Две простушки отправляются в евротур, чтобы спасти мир. Комедийная бондиана с Милой Кунис и Джиллиан Андерсон","editorAnnotation":null,"isTicketsAvailable":false,"productionStatus":null,"type":"FILM","ratingMpaa":"r","ratingAgeLimits":"age16","countries":[{"country":"США"},{"country":"Канада"},{"country":"Венгрия"}],"genres":[{"genre":"приключения"},{"genre":"боевик"},{"genre":"комедия"}],"startYear":null,"endYear":null,"serial":false,"shortFilm":false,"completed":false,"hasImax":false,"has3D":false,"lastSync":"2021-09-08T01:13:04.101546"} 47 | httpVersion: 1.1 48 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetSingleFilm_ShouldParseFilm_1142206.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v2.2/films/1142206 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:32 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | {"kinopoiskId":1142206,"imdbId":null,"nameRu":"Пальма","nameEn":null,"nameOriginal":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/posters/kp/1142206.jpg","posterUrlPreview":"https://kinopoiskapiunofficial.tech/images/posters/kp_small/1142206.jpg","reviewsCount":7,"ratingGoodReview":78.6,"ratingGoodReviewVoteCount":5,"ratingKinopoisk":7.3,"ratingKinopoiskVoteCount":18317,"ratingImdb":6.7,"ratingImdbVoteCount":148,"ratingFilmCritics":null,"ratingFilmCriticsVoteCount":0,"ratingAwait":89.84,"ratingAwaitCount":1792,"ratingRfCritics":100.0,"ratingRfCriticsVoteCount":6,"webUrl":"https://www.kinopoisk.ru/film/1142206/","year":2020,"filmLength":110,"slogan":"История настоящей дружбы","description":"Овчарка по кличке Пальма вынужденно расстается с хозяином: тот улетает за границу, а верную собаку не берут на рейс и оставляют прямо на летном поле. Пальма прячется в аэропорту и каждый день встречает самолеты в надежде, что хозяин вернулся. Но время идет… 9-летний Коля — тоже новенький в аэропорту: он потерял маму и переехал к отцу-пилоту, которого почти не знает. Пальма становится для мальчика родственной душой и лучшим другом. А отцу Коли, летчику Лазареву, предстоит заслужить доверие и любовь сына, сделав нелегкий выбор между карьерой и семьей. И найти способ не разлучить друзей, когда за Пальмой однажды возвращается хозяин.","shortDescription":null,"editorAnnotation":null,"isTicketsAvailable":false,"productionStatus":null,"type":"FILM","ratingMpaa":null,"ratingAgeLimits":"age6","countries":[{"country":"Япония"},{"country":"Россия"}],"genres":[{"genre":"приключения"},{"genre":"семейный"}],"startYear":null,"endYear":null,"serial":false,"shortFilm":false,"completed":false,"hasImax":false,"has3D":false,"lastSync":"2021-09-04T23:22:16.503729"} 47 | httpVersion: 1.1 48 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetSingleFilm_ShouldParseTvShow_77298.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v2.2/films/77298 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:31 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | {"kinopoiskId":77298,"imdbId":"tt0105946","nameRu":"Вавилон 5","nameEn":null,"nameOriginal":"Babylon 5","posterUrl":"https://kinopoiskapiunofficial.tech/images/posters/kp/77298.jpg","posterUrlPreview":"https://kinopoiskapiunofficial.tech/images/posters/kp_small/77298.jpg","reviewsCount":38,"ratingGoodReview":94.1,"ratingGoodReviewVoteCount":32,"ratingKinopoisk":8.1,"ratingKinopoiskVoteCount":10483,"ratingImdb":8.3,"ratingImdbVoteCount":29187,"ratingFilmCritics":null,"ratingFilmCriticsVoteCount":0,"ratingAwait":null,"ratingAwaitCount":0,"ratingRfCritics":null,"ratingRfCriticsVoteCount":0,"webUrl":"https://www.kinopoisk.ru/series/77298/","year":1993,"filmLength":43,"slogan":null,"description":"Началась новая эра в истории человечества. Минуло десять лет после войны Земли с Минбаром. Проект «Вавилон» стал воплощением мечты о галактике без войн. Его цель – создание центра, где различные цивилизации смогут решать спорные вопросы мирным путём. Станция стала местом встречи и домом для дипломатов, авантюристов, дельцов, путешественников, тысяч людей и инопланетян на двух с половиной миллионов тонн вращающегося металла, среди бездны космоса. Однако со временем «Вавилон 5» приобрёл другое значение и стал последней надеждой на прочный мир. Но вскоре эта надежда рухнула. И в час начала великих войн, он обрёл иное содержание, стал последней надеждой на победу. Время: 2257 года - 2382 год. Место действия: «Вавилон 5»","shortDescription":"Космическая станция становится центром межпланетных интриг. Классика сай-фая с детально продуманным миром","editorAnnotation":null,"isTicketsAvailable":false,"productionStatus":null,"type":"TV_SERIES","ratingMpaa":null,"ratingAgeLimits":"age12","countries":[{"country":"США"}],"genres":[{"genre":"драма"},{"genre":"фантастика"},{"genre":"приключения"},{"genre":"боевик"}],"startYear":1993,"endYear":1998,"serial":true,"shortFilm":false,"completed":false,"hasImax":false,"has3D":false,"lastSync":"2021-09-14T08:50:16.049974"} 47 | httpVersion: 1.1 48 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/KinopoiskApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace KinopoiskUnofficialInfo.ApiClient 10 | { 11 | public class KinopoiskApiClient : IKinopoiskApiClient 12 | { 13 | private readonly string _apiToken; 14 | private readonly ILogger _logger; 15 | private readonly IHttpClientFactory _httpClientFactory; 16 | private readonly Client _apiClient; 17 | 18 | public KinopoiskApiClient(string apiToken, ILogger logger, IHttpClientFactory httpClientFactory) 19 | { 20 | if (string.IsNullOrEmpty(apiToken)) 21 | { 22 | throw new System.ArgumentException($"'{nameof(apiToken)}' cannot be null or empty.", nameof(apiToken)); 23 | } 24 | 25 | _apiToken = apiToken; 26 | _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); 27 | _httpClientFactory = httpClientFactory ?? throw new System.ArgumentNullException(nameof(httpClientFactory)); 28 | 29 | var httpClient = _httpClientFactory.CreateClient(); 30 | httpClient.DefaultRequestHeaders.Add("X-API-KEY", _apiToken); 31 | _apiClient = new Client(httpClient); 32 | } 33 | 34 | private async Task Invoke(Func> method, CancellationToken? ct, [CallerMemberName] string memberName = "") 35 | { 36 | try 37 | { 38 | _logger.LogDebug($"{memberName} request starting..."); 39 | var res = await method.Invoke(ct ?? CancellationToken.None); 40 | _logger.LogDebug($"{memberName} request complete successfully"); 41 | return res; 42 | } 43 | catch (ApiException e) 44 | { 45 | _logger.LogError($"Received non-success result status code {e.StatusCode} from Kinopoisk API, response content is:\n{e.Response}"); 46 | throw; 47 | } 48 | } 49 | 50 | public Task GetSingleFilm(int filmId, CancellationToken? cancellationToken = null) 51 | => Invoke((ct) => _apiClient.FilmsAsync(filmId, ct), cancellationToken); 52 | 53 | public Task> GetStaff(int filmId, CancellationToken? cancellationToken = null) 54 | => Invoke((ct) => _apiClient.StaffAllAsync(filmId, ct), cancellationToken); 55 | 56 | public Task SearchByKeyword(string keyword, int page = 1, CancellationToken? cancellationToken = null) 57 | => Invoke((ct) => _apiClient.SearchByKeywordAsync(keyword, null, ct), cancellationToken); 58 | 59 | public Task GetPerson(int personId, CancellationToken? cancellationToken = null) 60 | => Invoke((ct) => _apiClient.StaffAsync(personId, ct), cancellationToken); 61 | 62 | public Task GetTrailers(int filmId, CancellationToken? cancellationToken = null) 63 | { 64 | return Invoke(async (ct) => { 65 | try { 66 | return await _apiClient.VideosAsync(filmId, ct); 67 | } catch (ApiException e) 68 | { 69 | if (e.StatusCode == 404) 70 | return new VideoResponse(); 71 | throw; 72 | } 73 | }, cancellationToken); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "regenerate-api-proxy", 8 | "command": "dotnet", 9 | "type": "process", 10 | "args": [ 11 | "msbuild", "-target:GenerateApiClientSourceCode", 12 | "${workspaceFolder:workspace}/src/KinopoiskUnofficialInfo.ApiClient/KinopoiskUnofficialInfo.ApiClient.csproj", 13 | // Ask dotnet build to generate full paths for file names. 14 | "-property:GenerateFullPaths=true", 15 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 16 | "-consoleloggerparameters:NoSummary" 17 | ], 18 | "group": "build", 19 | "problemMatcher": "$msCompile" 20 | }, 21 | { 22 | "label": "build", 23 | "command": "dotnet", 24 | "type": "process", 25 | "args": [ 26 | "build", 27 | "${workspaceFolder:workspace}/src/Jellyfin.Plugin.Kinopoisk.sln", 28 | // Ask dotnet build to generate full paths for file names. 29 | "-property:GenerateFullPaths=true", 30 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 31 | "-consoleloggerparameters:NoSummary" 32 | ], 33 | "group": "build", 34 | "problemMatcher": "$msCompile", 35 | }, 36 | { 37 | "label": "publish-to-run", 38 | "command": "dotnet", 39 | "type": "process", 40 | "args": [ 41 | "publish", 42 | "${workspaceFolder:workspace}/src/Jellyfin.Plugin.Kinopoisk/Jellyfin.Plugin.Kinopoisk.csproj", 43 | "--output", 44 | "/jellyfin-data/plugins/kinopoisk", 45 | "-property:GenerateFullPaths=true", 46 | "-property:Version=10.9.0.1" 47 | ], 48 | "group": "build", 49 | "presentation": { 50 | "reveal": "silent" 51 | }, 52 | "problemMatcher": "$msCompile", 53 | "dependsOn": ["clear-plugin-state-before-debug"] 54 | }, 55 | { 56 | "label": "clear-plugin-state-before-debug", 57 | "command": "rm", 58 | "type": "process", 59 | "args": [ 60 | "-f", "/jellyfin-data/plugins/kinopoisk/meta.json" 61 | ], 62 | "group": "build", 63 | "presentation": { 64 | "reveal": "silent" 65 | }, 66 | "problemMatcher": "$msCompile" 67 | }, 68 | { 69 | "label": "make-dist", 70 | "command": "jprm", 71 | "type": "process", 72 | "args": [ 73 | "-v", "DEBUG", 74 | "plugin", "build", "${workspaceFolder:workspace}/src/Jellyfin.Plugin.Kinopoisk/" 75 | ], 76 | "options": { 77 | "cwd": "${workspaceFolder:workspace}/" 78 | }, 79 | "group": "none", 80 | "presentation": { 81 | "reveal": "always" 82 | }, 83 | "problemMatcher": "$msCompile" 84 | }, 85 | { 86 | "label": "make-dist-publish", 87 | "command": "bash", 88 | "type": "process", 89 | "args": [ 90 | "${workspaceFolder:workspace}/publish.sh" 91 | ], 92 | "options": { 93 | "cwd": "${workspaceFolder:workspace}/" 94 | }, 95 | "group": "none", 96 | "presentation": { 97 | "reveal": "always" 98 | }, 99 | "problemMatcher": "$msCompile", 100 | "dependsOn": ["make-dist"] 101 | } 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/TransliterationStringExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright: https://calmsen.ru/transliteraciya-na-c 2 | // Copyright: http://usanov.net/748-transliteraciya-rus-2-lat-na-c 3 | 4 | using System.Collections.Generic; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace Jellyfin.Plugin.Kinopoisk 8 | { 9 | public static class TransliterationStringExtensions 10 | { 11 | //ISO 9-95 12 | private static Dictionary _replaceDictionary = new() 13 | { 14 | { "Є", "Ye" }, 15 | { "І", "I" }, 16 | { "Ѓ", "G" }, 17 | { "і", "i" }, 18 | { "№", "#" }, 19 | { "є", "ye" }, 20 | { "ѓ", "g" }, 21 | { "А", "A" }, 22 | { "Б", "B" }, 23 | { "В", "V" }, 24 | { "Г", "G" }, 25 | { "Д", "D" }, 26 | { "Е", "E" }, 27 | { "Ё", "Yo" }, 28 | { "Ж", "Zh" }, 29 | { "З", "Z" }, 30 | { "И", "I" }, 31 | { "Й", "J" }, 32 | { "К", "K" }, 33 | { "Л", "L" }, 34 | { "М", "M" }, 35 | { "Н", "N" }, 36 | { "О", "O" }, 37 | { "П", "P" }, 38 | { "Р", "R" }, 39 | { "С", "S" }, 40 | { "Т", "T" }, 41 | { "У", "U" }, 42 | { "Ф", "F" }, 43 | { "Х", "X" }, 44 | { "Ц", "C" }, 45 | { "Ч", "Ch" }, 46 | { "Ш", "Sh" }, 47 | { "Щ", "Shh" }, 48 | { "Ъ", "'" }, 49 | { "Ы", "Y" }, 50 | { "Ь", "" }, 51 | { "Э", "E" }, 52 | { "Ю", "Yu" }, 53 | { "Я", "Ya" }, 54 | { "а", "a" }, 55 | { "б", "b" }, 56 | { "в", "v" }, 57 | { "г", "g" }, 58 | { "д", "d" }, 59 | { "е", "e" }, 60 | { "ё", "yo" }, 61 | { "ж", "zh" }, 62 | { "з", "z" }, 63 | { "и", "i" }, 64 | { "й", "j" }, 65 | { "к", "k" }, 66 | { "л", "l" }, 67 | { "м", "m" }, 68 | { "н", "n" }, 69 | { "о", "o" }, 70 | { "п", "p" }, 71 | { "р", "r" }, 72 | { "с", "s" }, 73 | { "т", "t" }, 74 | { "у", "u" }, 75 | { "ф", "f" }, 76 | { "х", "x" }, 77 | { "ц", "c" }, 78 | { "ч", "ch" }, 79 | { "ш", "sh" }, 80 | { "щ", "shh" }, 81 | { "ъ", "" }, 82 | { "ы", "y" }, 83 | { "ь", "" }, 84 | { "э", "e" }, 85 | { "ю", "yu" }, 86 | { "я", "ya" }, 87 | }; 88 | 89 | public static string TransliterateToLatin(this string text) 90 | { 91 | string output = text; 92 | 93 | output = Regex.Replace(output, @"\s|\.|\(", " "); 94 | output = Regex.Replace(output, @"\s+", " "); 95 | output = Regex.Replace(output, @"[^\s\w\d-]", ""); 96 | output = output.Trim(); 97 | 98 | foreach (KeyValuePair key in _replaceDictionary) 99 | { 100 | output = output.Replace(key.Key, key.Value); 101 | } 102 | return output; 103 | } 104 | 105 | public static string TransliterateToCyrillic(this string text) 106 | { 107 | string output = text; 108 | 109 | foreach (KeyValuePair key in _replaceDictionary) 110 | { 111 | output = output.Replace(key.Value, key.Key); 112 | } 113 | return output; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient/CachedApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Reflection; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Caching.Memory; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace KinopoiskUnofficialInfo.ApiClient 11 | { 12 | public class CachedKinopoiskApiClient : IKinopoiskApiClient 13 | { 14 | private readonly IKinopoiskApiClient _innerClient; 15 | private readonly IMemoryCache _cache; 16 | private readonly ILogger _logger; 17 | 18 | public CachedKinopoiskApiClient(IKinopoiskApiClient innerClient, IMemoryCache cache, ILogger logger) 19 | { 20 | _innerClient = innerClient ?? throw new ArgumentNullException(nameof(innerClient)); 21 | _cache = cache ?? throw new ArgumentNullException(nameof(cache)); 22 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 23 | } 24 | 25 | public CachedKinopoiskApiClient(string apiToken, ILogger innerLogger, IHttpClientFactory httpClientFactory, IMemoryCache cache, ILogger logger) 26 | : this(new KinopoiskApiClient(apiToken, innerLogger, httpClientFactory), cache, logger) 27 | { 28 | } 29 | 30 | public Task GetPerson(int personId, CancellationToken? cancellationToken = null) 31 | => TryGetValue(GenerateKey(nameof(GetPerson), personId), c => c.GetPerson(personId, cancellationToken)); 32 | 33 | public Task GetSingleFilm(int filmId, CancellationToken? cancellationToken = null) 34 | => TryGetValue(GenerateKey(nameof(GetSingleFilm), filmId), c => c.GetSingleFilm(filmId, cancellationToken)); 35 | 36 | public Task> GetStaff(int filmId, CancellationToken? cancellationToken = null) 37 | => TryGetValue(GenerateKey(nameof(GetStaff), filmId), c => c.GetStaff(filmId, cancellationToken)); 38 | 39 | public Task GetTrailers(int filmId, CancellationToken? cancellationToken = null) 40 | => TryGetValue(GenerateKey(nameof(GetTrailers), filmId), c => c.GetTrailers(filmId, cancellationToken)); 41 | 42 | public Task SearchByKeyword(string keyword, int page = 1, CancellationToken? cancellationToken = null) 43 | => TryGetValue(GenerateKey(nameof(SearchByKeyword), keyword, page), c => c.SearchByKeyword(keyword, page, cancellationToken)); 44 | 45 | private static string GenerateKey(params object[] objects) 46 | { 47 | var key = string.Empty; 48 | 49 | foreach (var obj in objects) 50 | { 51 | var objType = obj.GetType(); 52 | if (objType.IsPrimitive || objType == typeof(string)) 53 | { 54 | key += obj + ";"; 55 | } 56 | else 57 | { 58 | foreach (PropertyInfo propertyInfo in objType.GetProperties()) 59 | { 60 | var currentValue = propertyInfo.GetValue(obj, null); 61 | if (currentValue == null) 62 | { 63 | continue; 64 | } 65 | 66 | key += propertyInfo.Name + "=" + currentValue + ";"; 67 | } 68 | } 69 | } 70 | 71 | return key; 72 | } 73 | 74 | private Task TryGetValue(string key, Func> resultFactory) 75 | { 76 | return _cache.GetOrCreateAsync(key, async entry => 77 | { 78 | _logger.LogDebug($"Entry '{key}' not found in cache, requesting from server"); 79 | entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1); 80 | 81 | var result = await resultFactory.Invoke(_innerClient).ConfigureAwait(false); 82 | 83 | return result; 84 | }); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | VERSION="10.9.0.1" 3 | CHANGELOG="Fix bugs" 4 | 5 | check_command() { 6 | if ! command -v $1 &> /dev/null 7 | then 8 | echo "Error: $1 could not be found. Please install it." 9 | exit 1 10 | fi 11 | } 12 | 13 | # Check for required commands 14 | check_command gsed 15 | 16 | brew link --overwrite dotnet@8 17 | export PATH="/usr/local/opt/dotnet@8/bin:$PATH" 18 | 19 | find . -name project.assets.json -delete 20 | 21 | gsed -i'' "s/version: .*/version: \"$VERSION\"/" src/Jellyfin.Plugin.Kinopoisk/build.yaml 22 | BUILDYAML=`head -$(grep -n "changelog: >" src/Jellyfin.Plugin.Kinopoisk/build.yaml | head -1 | cut -d: -f1) src/Jellyfin.Plugin.Kinopoisk/build.yaml` 23 | echo -e "$BUILDYAML\n $CHANGELOG" > src/Jellyfin.Plugin.Kinopoisk/build.yaml 24 | 25 | dotnet restore ./src/Jellyfin.Plugin.Kinopoisk/ 26 | dotnet build --configuration Release ./src/Jellyfin.Plugin.Kinopoisk/ 27 | 28 | RELEASEDIR="$(pwd)/dist/kinopoisk/kinopoisk_$VERSION" 29 | rm -rf "$RELEASEDIR" "$RELEASEDIR.zip" 30 | mkdir -p "$RELEASEDIR" 31 | cp "$(pwd)/src/Jellyfin.Plugin.Kinopoisk/bin/Release/net8.0/Jellyfin.Plugin.Kinopoisk.dll" "$RELEASEDIR/" 32 | cp "$(pwd)/src/KinopoiskUnofficialInfo.ApiClient/bin/Release/net8.0/KinopoiskUnofficialInfo.ApiClient.dll" "$RELEASEDIR/" 33 | cat << EOF > "dist/kinopoisk/kinopoisk_$VERSION/meta.json" 34 | { 35 | "category": "Metadata", 36 | "changelog": "$CHANGELOG", 37 | "description": "\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 \u0440\u0435\u0439\u0442\u0438\u043d\u0433, \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f, \u0430\u043a\u0442\u0451\u0440\u043e\u0432, \u0442\u0440\u0435\u0439\u043b\u0435\u0440\u044b \u0438 \u0442.\u0434. \u0441 \u0441\u0430\u0439\u0442\u0430 \u041a\u0438\u043d\u043e\u041f\u043e\u0438\u0441\u043a. \u041c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u0439 ApiToken, \u0441\u043c. \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0432 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0445 \u043f\u043b\u0430\u0433\u0438\u043d\u0430. \u0414\u043b\u044f \u0442\u043e\u0447\u043d\u043e\u0433\u043e \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u044f \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c id \u0444\u0438\u043b\u044c\u043c\u0430 \u0441 \u0441\u0430\u0439\u0442\u0430 \u041a\u0438\u043d\u043e\u041f\u043e\u0438\u0441\u043a \u0432 \u0438\u043c\u0435\u043d\u0438 \u0444\u0430\u0439\u043b\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 kp-12345 \u0438\u043b\u0438 kp12345. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0441\u043c. https://github.com/LinFor/jellyfin-plugin-kinopoisk/blob/master/README.md\n", 38 | "guid": "0c136f8a-ff77-4f2b-ade5-13462cae6216", 39 | "imageUrl": "https://kinopoisk.userecho.com/s/attachments/28876/0/1/25f8c0315e6ccb2aa6c2642e48f2c9e9.png", 40 | "name": "\u041a\u0438\u043d\u043e\u041f\u043e\u0438\u0441\u043a", 41 | "overview": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0444\u0438\u043b\u044c\u043c\u0430\u0445 \u0438 \u0441\u0435\u0440\u0438\u0430\u043b\u0430\u0445 \u0441 \u041a\u0438\u043d\u043e\u041f\u043e\u0438\u0441\u043a\u0430", 42 | "owner": "svk", 43 | "targetAbi": "10.9.0", 44 | "timestamp": "$(date -u "+%Y-%m-%dT%H:%M:%SZ")", 45 | "version": "$VERSION" 46 | } 47 | EOF 48 | echo $( cd $RELEASEDIR; zip -j "../kinopoisk_$VERSION.zip" *) 49 | rm -rf "$RELEASEDIR" 50 | HASH=$(md5sum "$RELEASEDIR.zip" | cut -d' ' -f1) 51 | 52 | jq --arg HASH "$HASH" --arg URL "https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_$VERSION.zip" \ 53 | --arg TIMESTAMP "$(date -u "+%Y-%m-%dT%H:%M:%SZ")" \ 54 | --arg VERSION "$VERSION" \ 55 | '.[0].versions |= [{"version": $VERSION, "checksum": $HASH, "changelog": "new release", "name": "\u041a\u0438\u043d\u043e\u041f\u043e\u0438\u0441\u043a", "targetAbi": "10.9.0", "sourceUrl": $URL, "timestamp": $TIMESTAMP}] + .' \ 56 | "$(pwd)/dist/manifest.json" > "$(pwd)/dist/manifest.json.tmp" && \ 57 | mv "$(pwd)/dist/manifest.json.tmp" "$(pwd)/dist/manifest.json" 58 | exit 59 | #jprm repo add -u https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/master/dist/ ./dist ./artifacts/*.zip 60 | rm -rf ./artifacts/* 61 | git add "$RELEASEDIR.zip" "dist/manifest.json" "publish.sh" "src/Jellyfin.Plugin.Kinopoisk/build.yaml" && \ 62 | git commit -m "version $VERSION" && \ 63 | git tag -f "v$VERSION" #&& \ 64 | git push --force && git push --tags --force 65 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/MetadataProviders/BaseVideoMetadataProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Jellyfin.Plugin.Kinopoisk.ProviderIdResolvers; 7 | using KinopoiskUnofficialInfo.ApiClient; 8 | using MediaBrowser.Controller.Entities; 9 | using MediaBrowser.Controller.Providers; 10 | using MediaBrowser.Model.Entities; 11 | using MediaBrowser.Model.Providers; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace Jellyfin.Plugin.Kinopoisk.MetadataProviders 15 | { 16 | public abstract class BaseVideoMetadataProvider : BaseMetadataProvider, IRemoteMetadataProvider 17 | where TItemType : BaseItem, IHasLookupInfo 18 | where TLookupInfoType : ItemLookupInfo, new() 19 | { 20 | private readonly ILogger _logger; 21 | private readonly IKinopoiskApiClient _apiClient; 22 | private readonly IProviderIdResolver _providerIdResolver; 23 | 24 | public BaseVideoMetadataProvider(IKinopoiskApiClient kinopoiskApiClient, IProviderIdResolver providerIdResolver, ILogger logger, IHttpClientFactory httpClientFactory) 25 | : base(httpClientFactory) 26 | { 27 | _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); 28 | _apiClient = kinopoiskApiClient ?? throw new System.ArgumentNullException(nameof(kinopoiskApiClient)); 29 | _providerIdResolver = providerIdResolver ?? throw new System.ArgumentNullException(nameof(providerIdResolver)); 30 | } 31 | 32 | protected abstract TItemType ConvertResponseToItem(Film apiResponse); 33 | 34 | public async Task> GetMetadata(TLookupInfoType info, CancellationToken cancellationToken) 35 | { 36 | var result = new MetadataResult() 37 | { 38 | QueriedById = true, 39 | Provider = Constants.ProviderName, 40 | ResultLanguage = Constants.ProviderMetadataLanguage 41 | }; 42 | 43 | var (resolveResult, kinopoiskId) = await _providerIdResolver.TryResolve(info, cancellationToken); 44 | if (!resolveResult) 45 | return result; 46 | 47 | var film = await _apiClient.GetSingleFilm(kinopoiskId, cancellationToken); 48 | 49 | cancellationToken.ThrowIfCancellationRequested(); 50 | 51 | result.Item = ConvertResponseToItem(film); 52 | if (result.Item != null) 53 | result.HasMetadata = true; 54 | 55 | var staff = await _apiClient.GetStaff(kinopoiskId, cancellationToken); 56 | 57 | cancellationToken.ThrowIfCancellationRequested(); 58 | 59 | var sanitizedPersons = await SanitizeEmptyImagePersonInfos(staff.ToPersonInfos()); 60 | foreach (var item in sanitizedPersons) 61 | result.AddPerson(item); 62 | 63 | var trailers = await _apiClient.GetTrailers(kinopoiskId, cancellationToken); 64 | 65 | var remoteTrailers = trailers.ToMediaUrls(); 66 | if (remoteTrailers is not null) 67 | result.Item.RemoteTrailers = remoteTrailers; 68 | 69 | return result; 70 | } 71 | 72 | public async Task> GetSearchResults(TLookupInfoType searchInfo, CancellationToken cancellationToken) 73 | { 74 | if (searchInfo.TryGetProviderId(Constants.ProviderId, out var kinopoiskIdStr) 75 | && int.TryParse(kinopoiskIdStr, out var kinopoiskId)) 76 | { 77 | var singleResult = (await _apiClient.GetSingleFilm(kinopoiskId, cancellationToken)).ToRemoteSearchResult(); 78 | return Enumerable.Repeat(singleResult, 1); 79 | } 80 | else 81 | { 82 | return (await _apiClient.SearchByKeyword(searchInfo.Name, cancellationToken: cancellationToken)).ToRemoteSearchResults(_logger); 83 | } 84 | } 85 | 86 | protected async Task> SanitizeEmptyImagePersonInfos(IEnumerable images) 87 | { 88 | using var httpClient = new HttpClient(new HttpClientHandler() { AllowAutoRedirect = false }, true); 89 | var sanitizer = new RemoteImageUrlSanitizer(httpClient); 90 | var res = await Task.WhenAll(images.Select(async p => { 91 | p.ImageUrl = await sanitizer.SanitizeRemoteImageUrl(p.ImageUrl); 92 | return p; 93 | })); 94 | 95 | return res.Where(i => i != null).ToArray(); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/node-debian.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/master/script-library/docs/node.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag] 11 | 12 | export NVM_DIR=${1:-"/usr/local/share/nvm"} 13 | export NODE_VERSION=${2:-"lts/*"} 14 | USERNAME=${3:-"automatic"} 15 | UPDATE_RC=${4:-"true"} 16 | 17 | set -e 18 | 19 | if [ "$(id -u)" -ne 0 ]; then 20 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 21 | exit 1 22 | fi 23 | 24 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 25 | rm -f /etc/profile.d/00-restore-env.sh 26 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 27 | chmod +x /etc/profile.d/00-restore-env.sh 28 | 29 | # Determine the appropriate non-root user 30 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 31 | USERNAME="" 32 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 33 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 34 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 35 | USERNAME=${CURRENT_USER} 36 | break 37 | fi 38 | done 39 | if [ "${USERNAME}" = "" ]; then 40 | USERNAME=root 41 | fi 42 | elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then 43 | USERNAME=root 44 | fi 45 | 46 | if [ "${NODE_VERSION}" = "none" ]; then 47 | export NODE_VERSION= 48 | fi 49 | 50 | function updaterc() { 51 | if [ "${UPDATE_RC}" = "true" ]; then 52 | echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." 53 | echo -e "$1" >> /etc/bash.bashrc 54 | if [ -f "/etc/zsh/zshrc" ]; then 55 | echo -e "$1" >> /etc/zsh/zshrc 56 | fi 57 | fi 58 | } 59 | 60 | # Ensure apt is in non-interactive to avoid prompts 61 | export DEBIAN_FRONTEND=noninteractive 62 | 63 | # Install curl, apt-transport-https, tar, or gpg if missing 64 | if ! dpkg -s apt-transport-https curl ca-certificates tar > /dev/null 2>&1 || ! type gpg > /dev/null 2>&1; then 65 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 66 | apt-get update 67 | fi 68 | apt-get -y install --no-install-recommends apt-transport-https curl ca-certificates tar gnupg2 69 | fi 70 | 71 | # Install yarn 72 | if type yarn > /dev/null 2>&1; then 73 | echo "Yarn already installed." 74 | else 75 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | (OUT=$(apt-key add - 2>&1) || echo $OUT) 76 | echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 77 | apt-get update 78 | apt-get -y install --no-install-recommends yarn 79 | fi 80 | 81 | # Install the specified node version if NVM directory already exists, then exit 82 | if [ -d "${NVM_DIR}" ]; then 83 | echo "NVM already installed." 84 | if [ "${NODE_VERSION}" != "" ]; then 85 | su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" 86 | fi 87 | exit 0 88 | fi 89 | 90 | # Create nvm group, nvm dir, and set sticky bit 91 | if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then 92 | groupadd -r nvm 93 | fi 94 | umask 0002 95 | usermod -a -G nvm ${USERNAME} 96 | mkdir -p ${NVM_DIR} 97 | chown :nvm ${NVM_DIR} 98 | chmod g+s ${NVM_DIR} 99 | su ${USERNAME} -c "$(cat << EOF 100 | set -e 101 | umask 0002 102 | # Do not update profile - we'll do this manually 103 | export PROFILE=/dev/null 104 | curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash 105 | source ${NVM_DIR}/nvm.sh 106 | if [ "${NODE_VERSION}" != "" ]; then 107 | nvm alias default ${NODE_VERSION} 108 | fi 109 | nvm clear-cache 110 | EOF 111 | )" 2>&1 112 | # Update rc files 113 | if [ "${UPDATE_RC}" = "true" ]; then 114 | updaterc "$(cat < : CommonLookupInfoResolver 14 | where T : ItemLookupInfo 15 | { 16 | private readonly IKinopoiskApiClient _kinopoiskApiClient; 17 | 18 | public VideoResolver(IKinopoiskApiClient kinopoiskApiClient, ILogger> logger) : base(logger) 19 | { 20 | _kinopoiskApiClient = kinopoiskApiClient ?? throw new ArgumentNullException(nameof(kinopoiskApiClient)); 21 | } 22 | 23 | public override async Task<(bool IsSuccess, int ProviderId)> TryResolve(T info, CancellationToken? ct = null) 24 | { 25 | // Try to get from standart sources 26 | var possibleResult = await base.TryResolve(info, ct); 27 | if (possibleResult.IsSuccess) 28 | return possibleResult; 29 | 30 | // Trying to find empirically on kinopoisk 31 | if (string.IsNullOrWhiteSpace(info.Name)) 32 | { 33 | _logger.LogDebug($"Film name is empty, skipping KinopoiskProviderId search"); 34 | return (false, 0); 35 | } 36 | 37 | _logger.LogDebug($"Trying to get suitable film with name '{info.Name}'..."); 38 | var searchResult = await _kinopoiskApiClient.SearchByKeyword(info.Name, 1, ct ?? CancellationToken.None); 39 | if (searchResult.SearchFilmsCountResult < 1 || searchResult?.Films.Count < 1) 40 | { 41 | _logger.LogDebug($"Received empty search result"); 42 | return (false, 0); 43 | } 44 | var candidates = searchResult.Films.ToArray(); 45 | _logger.LogDebug($"Received {candidates.Length} results, trying to filter and match..."); 46 | 47 | // Check if there are single candidate filtered by year 48 | possibleResult = await TryResolveBySingleCandidateLeft(info, FilterByYear(info, candidates), ct); 49 | if (possibleResult.IsSuccess) 50 | return possibleResult; 51 | 52 | // Try to resolve by ImdbId match filtered by year 53 | possibleResult = await TryResolveByImdbMatch(info, FilterByYear(info, candidates), ct); 54 | if (possibleResult.IsSuccess) 55 | return possibleResult; 56 | 57 | // Try to resolve by ImdbId match without filtering 58 | possibleResult = await TryResolveByImdbMatch(info, candidates, ct); 59 | if (possibleResult.IsSuccess) 60 | return possibleResult; 61 | 62 | _logger.LogDebug($"Suitable result not found"); 63 | return (false, 0); 64 | } 65 | 66 | public async Task<(bool IsSuccess, int ProviderId)> TryResolveByImdbMatch(T info, ICollection candidates, CancellationToken? ct = null) 67 | { 68 | if (info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId)) 69 | { 70 | _logger.LogDebug($"Trying to find result with ImdbId '{imdbId}'..."); 71 | var index = 0; 72 | foreach (var candidate in candidates) 73 | { 74 | try 75 | { 76 | var film = await _kinopoiskApiClient.GetSingleFilm(candidate.FilmId, ct); 77 | 78 | if (imdbId == film?.ImdbId) 79 | { 80 | _logger.LogDebug($"Found match: {candidate.FilmId} '{film.GetLocalName()}', ImdbId '{film?.ImdbId}', setting KinopoiskProviderId to {candidate.FilmId}"); 81 | return (true, candidate.FilmId); 82 | } 83 | 84 | _logger.LogDebug($"Film {candidate.FilmId} '{film.GetLocalName()}' has ImdbId '{film?.ImdbId}', skipping, {candidates.Count - ++index} candidates left..."); 85 | } catch (Exception e) 86 | { 87 | _logger.LogError(e, $"Error while retrieving film {candidate.FilmId}"); 88 | continue; 89 | } 90 | } 91 | } 92 | 93 | return (false, 0); 94 | } 95 | 96 | public Task<(bool IsSuccess, int ProviderId)> TryResolveBySingleCandidateLeft(T info, ICollection candidates, CancellationToken? ct = null) 97 | { 98 | if (candidates.Count == 1) 99 | { 100 | var kinopoiskId = candidates.Single().FilmId; 101 | _logger.LogDebug($"There is single candidate left, setting KinopoiskProviderId to {kinopoiskId} ({info.Name})"); 102 | return Task.FromResult((true, kinopoiskId)); 103 | } 104 | 105 | return Task.FromResult((false, 0)); 106 | } 107 | 108 | public ICollection FilterByYear(T info, ICollection candidates) 109 | { 110 | if (!info.Year.HasValue) 111 | { 112 | _logger.LogDebug($"Can't filter by year, no year set in metadata..."); 113 | return Array.Empty(); 114 | } 115 | 116 | var targetYear = info.Year.Value.ToString(); 117 | var res = candidates.Where(f => f.Year == targetYear).ToArray(); 118 | _logger.LogDebug($"Filtered by year {targetYear}, {res.Length} results left..."); 119 | return res; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/KinopoiskApiTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net.Http; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Logging.Abstractions; 7 | using Moq; 8 | using Vcr; 9 | using Xunit; 10 | 11 | namespace KinopoiskUnofficialInfo.ApiClient.Tests 12 | { 13 | public class KinopoiskApiClientTests 14 | { 15 | private const string ApiToken = "85d30ae5-d875-4c5f-900d-8e37bb20625e"; 16 | private readonly ILoggerFactory _loggerFactory = NullLoggerFactory.Instance; 17 | private readonly VCR _vcr; 18 | private readonly Mock _clientFactoryMock; 19 | public KinopoiskApiClientTests() 20 | { 21 | var dirInfo = new System.IO.DirectoryInfo("../../../cassettes"); //3 levels up to get to the root of the test project 22 | _vcr = new VCR(new FileSystemCassetteStorage(dirInfo)); 23 | 24 | _clientFactoryMock = new Mock(); 25 | _clientFactoryMock 26 | .Setup(f => f.CreateClient(It.IsAny())) 27 | .Returns(() => 28 | { 29 | var httpHandler = _vcr.GetVcrHandler(); 30 | httpHandler.InnerHandler = new HttpClientHandler(); 31 | return new HttpClient(httpHandler); 32 | }); 33 | } 34 | 35 | private string GetMethodName([CallerMemberName] string memberName = "") => memberName; 36 | 37 | [Theory] 38 | [InlineData(1142206, "Пальма", null)] 39 | [InlineData(1044982, "Шпион, который меня кинул", "The Spy Who Dumped Me")] 40 | [InlineData(1445243, "Будь моим Кириллом", null)] 41 | [InlineData(251733, "Аватар", "Avatar")] 42 | public async Task GetSingleFilm_ShouldParseFilm(int filmId, string nameRu, string nameOriginal) 43 | { 44 | using (_vcr.UseCassette($"{GetMethodName()}_{filmId}", RecordMode.NewEpisodes)) 45 | { 46 | var apiClient = new KinopoiskApiClient(ApiToken, _loggerFactory.CreateLogger(), _clientFactoryMock.Object); 47 | 48 | var res = await apiClient.GetSingleFilm(filmId); 49 | 50 | Assert.NotNull(res); 51 | Assert.Equal(FilmType.FILM, res.Type); 52 | Assert.Equal(nameRu, res.NameRu); 53 | Assert.Equal(nameOriginal, res.NameOriginal); 54 | } 55 | } 56 | 57 | [Theory] 58 | [InlineData(4416198, "В активном поиске", null, FilmType.TV_SERIES)] 59 | [InlineData(77298, "Вавилон 5", "Babylon 5", FilmType.TV_SERIES)] 60 | public async Task GetSingleFilm_ShouldParseTvShow(int filmId, string nameRu, string nameOriginal, FilmType type) 61 | { 62 | using (_vcr.UseCassette($"{GetMethodName()}_{filmId}", RecordMode.NewEpisodes)) 63 | { 64 | var apiClient = new KinopoiskApiClient(ApiToken, _loggerFactory.CreateLogger(), _clientFactoryMock.Object); 65 | 66 | var res = await apiClient.GetSingleFilm(filmId); 67 | 68 | Assert.NotNull(res); 69 | Assert.Equal(type, res.Type); 70 | Assert.Equal(nameRu, res.NameRu); 71 | Assert.Equal(nameOriginal, res.NameOriginal); 72 | } 73 | } 74 | 75 | [Theory] 76 | [InlineData(89540, 81, StaffResponseProfessionKey.PRODUCER_USSR)] 77 | public async Task GetSingleFilm_ShouldParseProducerUssr(int filmId, int index, StaffResponseProfessionKey profession) 78 | { 79 | using (_vcr.UseCassette($"{GetMethodName()}_{filmId}", RecordMode.NewEpisodes)) 80 | { 81 | var apiClient = new KinopoiskApiClient(ApiToken, _loggerFactory.CreateLogger(), _clientFactoryMock.Object); 82 | 83 | var res = await apiClient.GetStaff(filmId); 84 | 85 | Assert.NotNull(res); 86 | Assert.Equal(profession, res.ToArray()[index].ProfessionKey); 87 | } 88 | } 89 | 90 | [Theory] 91 | [InlineData(3873197, "Ирина Старшенбаум", "")] 92 | public async Task GetPerson_ShouldParseName(int personId, string nameRu, string nameEn) 93 | { 94 | using (_vcr.UseCassette($"{GetMethodName()}_{personId}", RecordMode.NewEpisodes)) 95 | { 96 | var apiClient = new KinopoiskApiClient(ApiToken, _loggerFactory.CreateLogger(), _clientFactoryMock.Object); 97 | 98 | var res = await apiClient.GetPerson(personId); 99 | 100 | Assert.NotNull(res); 101 | Assert.Equal(nameRu, res.NameRu); 102 | Assert.Equal(nameEn, res.NameEn); 103 | } 104 | } 105 | 106 | [Theory] 107 | [InlineData(1395460, "Интернет-трейлер (сезон 1)")] 108 | public async Task GetTrailers_ShouldParseName(int filmId, string name) 109 | { 110 | using (_vcr.UseCassette($"{GetMethodName()}_{filmId}", RecordMode.NewEpisodes)) 111 | { 112 | var apiClient = new KinopoiskApiClient(ApiToken, _loggerFactory.CreateLogger(), _clientFactoryMock.Object); 113 | 114 | var res = await apiClient.GetTrailers(filmId); 115 | 116 | Assert.NotNull(res); 117 | Assert.Contains(res.Items, t => name.Equals(t.Name)); 118 | } 119 | } 120 | 121 | [Theory] 122 | [InlineData(948870)] 123 | public async Task GetTrailers_ShouldReturnEmptyResponse(int filmId) 124 | { 125 | using (_vcr.UseCassette($"{GetMethodName()}_{filmId}", RecordMode.NewEpisodes)) 126 | { 127 | var apiClient = new KinopoiskApiClient(ApiToken, _loggerFactory.CreateLogger(), _clientFactoryMock.Object); 128 | 129 | var res = await apiClient.GetTrailers(filmId); 130 | 131 | Assert.NotNull(res); 132 | Assert.Empty(res.Items); 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetPerson_ShouldParseName_3873197.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v1/staff/3873197 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:31 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | {"personId":3873197,"webUrl":"https://www.kinopoisk.ru/name/3873197/","nameRu":"Ирина Старшенбаум","nameEn":"","sex":"FEMALE","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/3873197.jpg","growth":170,"birthday":"1992-03-30","death":null,"age":29,"birthplace":"Москва, Россия","deathplace":null,"spouses":[],"hasAwards":1,"profession":"Актриса, Озвучка","facts":[],"films":[{"filmId":564847,"nameRu":"Дни кино","nameEn":"Días de cine","rating":"5.6","general":false,"description":"играет саму себя - интервьюируемая","professionKey":"HERSELF"},{"filmId":675211,"nameRu":"Вечерний Ургант","nameEn":null,"rating":"7.3","general":false,"description":"играет саму себя, гость","professionKey":"HERSELF"},{"filmId":840250,"nameRu":"Притяжение","nameEn":null,"rating":"5.6","general":false,"description":"Юля Лебедева","professionKey":"ACTOR"},{"filmId":869551,"nameRu":"Кроп","nameEn":null,"rating":null,"general":false,"description":"Юля","professionKey":"ACTOR"},{"filmId":893259,"nameRu":"Черная вода","nameEn":null,"rating":"5.1","general":false,"description":"Полина","professionKey":"ACTOR"},{"filmId":900052,"nameRu":"Лёд","nameEn":null,"rating":"6.8","general":false,"description":"конкурентка","professionKey":"ACTOR"},{"filmId":923146,"nameRu":"Крыша мира","nameEn":null,"rating":"6.9","general":false,"description":"Ольга Шубина","professionKey":"ACTOR"},{"filmId":925971,"nameRu":"Переезд","nameEn":null,"rating":"5.6","general":false,"description":"Настя","professionKey":"ACTOR"},{"filmId":930878,"nameRu":"Т-34","nameEn":null,"rating":"6.5","general":false,"description":"Аня","professionKey":"ACTOR"},{"filmId":940680,"nameRu":"Ольга","nameEn":null,"rating":"6.4","general":false,"description":"Света","professionKey":"ACTOR"},{"filmId":949641,"nameRu":"Подарок Веры","nameEn":null,"rating":"5.8","general":false,"description":"Вера","professionKey":"ACTOR"},{"filmId":988782,"nameRu":"Ральф против интернета","nameEn":"Ralph Breaks the Internet","rating":"7.2","general":false,"description":"Дженнифер Хейл","professionKey":"VOICE_FEMALE"},{"filmId":993589,"nameRu":"Килиманджара","nameEn":null,"rating":"5.1","general":false,"description":"Маруся","professionKey":"ACTOR"},{"filmId":993595,"nameRu":"Лачуга должника","nameEn":null,"rating":"6.4","general":false,"description":"Элла","professionKey":"ACTOR"},{"filmId":1006637,"nameRu":"Шакал","nameEn":null,"rating":"7.8","general":false,"description":"Калина","professionKey":"ACTOR"},{"filmId":1009413,"nameRu":"Лето","nameEn":null,"rating":"7.4","general":false,"description":"Наташа","professionKey":"ACTOR"},{"filmId":1045582,"nameRu":"Вторжение","nameEn":null,"rating":"5.7","general":false,"description":"Юля Лебедева","professionKey":"ACTOR"},{"filmId":1108687,"nameRu":"Учителя","nameEn":null,"rating":"6.8","general":false,"description":"Мария Сергеевна Григорьевна, младшая дочь Сергея","professionKey":"ACTOR"},{"filmId":1111854,"nameRu":"Все сложно","nameEn":null,"rating":"7.7","general":false,"description":"Катя","professionKey":"ACTOR"},{"filmId":1117965,"nameRu":"#Зановородиться","nameEn":null,"rating":"6.8","general":false,"description":"","professionKey":"ACTOR"},{"filmId":1144176,"nameRu":"Нет","nameEn":null,"rating":"6.4","general":false,"description":"","professionKey":"ACTOR"},{"filmId":1191043,"nameRu":"Содержанки","nameEn":null,"rating":"6.6","general":false,"description":"Ульяна","professionKey":"ACTOR"},{"filmId":1237609,"nameRu":null,"nameEn":"The Primary Talent","rating":null,"general":false,"description":"Angie Angel","professionKey":"ACTOR"},{"filmId":1243139,"nameRu":"Кощей. Начало","nameEn":null,"rating":null,"general":false,"description":"Мэй, озвучка","professionKey":"ACTOR"},{"filmId":1272376,"nameRu":"Шерлок в России","nameEn":null,"rating":"6.5","general":false,"description":"Софья","professionKey":"ACTOR"},{"filmId":1272376,"nameRu":"Шерлок в России","nameEn":null,"rating":"6.5","general":false,"description":"Софья Касаткина","professionKey":"ACTOR"},{"filmId":1311547,"nameRu":"Горизонт","nameEn":null,"rating":null,"general":false,"description":"Лена","professionKey":"ACTOR"},{"filmId":1251146,"nameRu":"Общага","nameEn":null,"rating":null,"general":false,"description":"Нелли","professionKey":"ACTOR"},{"filmId":1338227,"nameRu":"Содержанки 2","nameEn":null,"rating":"6.8","general":false,"description":"Ульяна","professionKey":"ACTOR"},{"filmId":1367772,"nameRu":"Коронный выход","nameEn":null,"rating":null,"general":false,"description":"играет саму себя, гость","professionKey":"HERSELF"},{"filmId":1374533,"nameRu":"Точка отрыва","nameEn":null,"rating":null,"general":false,"description":"","professionKey":"ACTOR"},{"filmId":1395460,"nameRu":"Медиатор","nameEn":null,"rating":"8.3","general":false,"description":"","professionKey":"ACTOR"},{"filmId":1395460,"nameRu":"Медиатор","nameEn":null,"rating":"8.3","general":false,"description":"Марина","professionKey":"ACTOR"},{"filmId":1402933,"nameRu":"Джетлаг","nameEn":null,"rating":"6.0","general":false,"description":"Женя Горчакова","professionKey":"ACTOR"},{"filmId":1402934,"nameRu":"Джетлаг","nameEn":null,"rating":"5.5","general":false,"description":"Женя","professionKey":"ACTOR"},{"filmId":1436045,"nameRu":"Инсомния","nameEn":null,"rating":null,"general":false,"description":"","professionKey":"ACTOR"},{"filmId":2000122,"nameRu":"Здоровый человек","nameEn":null,"rating":null,"general":false,"description":"Майя, жена Егора","professionKey":"ACTOR"},{"filmId":4407805,"nameRu":"Надвое","nameEn":null,"rating":null,"general":false,"description":"","professionKey":"ACTOR"}]} 47 | httpVersion: 1.1 48 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # With more recent updates Visual Studio 2017 supports EditorConfig files out of the box 2 | # Visual Studio Code needs an extension: https://github.com/editorconfig/editorconfig-vscode 3 | # For emacs, vim, np++ and other editors, see here: https://github.com/editorconfig 4 | ############################### 5 | # Core EditorConfig Options # 6 | ############################### 7 | root = true 8 | # All files 9 | [*] 10 | indent_style = space 11 | indent_size = 4 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | end_of_line = lf 16 | max_line_length = off 17 | 18 | # YAML indentation 19 | [*.{yml,yaml}] 20 | indent_size = 2 21 | 22 | # XML indentation 23 | [*.{csproj,xml}] 24 | indent_size = 2 25 | 26 | ############################### 27 | # .NET Coding Conventions # 28 | ############################### 29 | [*.{cs,vb}] 30 | # Organize usings 31 | dotnet_sort_system_directives_first = true 32 | # this. preferences 33 | dotnet_style_qualification_for_field = false:silent 34 | dotnet_style_qualification_for_property = false:silent 35 | dotnet_style_qualification_for_method = false:silent 36 | dotnet_style_qualification_for_event = false:silent 37 | # Language keywords vs BCL types preferences 38 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 39 | dotnet_style_predefined_type_for_member_access = true:silent 40 | # Parentheses preferences 41 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 42 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 43 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 44 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 45 | # Modifier preferences 46 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 47 | dotnet_style_readonly_field = true:suggestion 48 | # Expression-level preferences 49 | dotnet_style_object_initializer = true:suggestion 50 | dotnet_style_collection_initializer = true:suggestion 51 | dotnet_style_explicit_tuple_names = true:suggestion 52 | dotnet_style_null_propagation = true:suggestion 53 | dotnet_style_coalesce_expression = true:suggestion 54 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 55 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 56 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 57 | dotnet_style_prefer_auto_properties = true:silent 58 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 59 | dotnet_style_prefer_conditional_expression_over_return = true:silent 60 | 61 | ############################### 62 | # Naming Conventions # 63 | ############################### 64 | # Style Definitions (From Roslyn) 65 | 66 | # Non-private static fields are PascalCase 67 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 68 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 69 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 70 | 71 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 72 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 73 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 74 | 75 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 76 | 77 | # Constants are PascalCase 78 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 79 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 80 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 81 | 82 | dotnet_naming_symbols.constants.applicable_kinds = field, local 83 | dotnet_naming_symbols.constants.required_modifiers = const 84 | 85 | dotnet_naming_style.constant_style.capitalization = pascal_case 86 | 87 | # Static fields are camelCase and start with s_ 88 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion 89 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields 90 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style 91 | 92 | dotnet_naming_symbols.static_fields.applicable_kinds = field 93 | dotnet_naming_symbols.static_fields.required_modifiers = static 94 | 95 | dotnet_naming_style.static_field_style.capitalization = camel_case 96 | dotnet_naming_style.static_field_style.required_prefix = _ 97 | 98 | # Instance fields are camelCase and start with _ 99 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 100 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 101 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 102 | 103 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 104 | 105 | dotnet_naming_style.instance_field_style.capitalization = camel_case 106 | dotnet_naming_style.instance_field_style.required_prefix = _ 107 | 108 | # Locals and parameters are camelCase 109 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 110 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 111 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 112 | 113 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 114 | 115 | dotnet_naming_style.camel_case_style.capitalization = camel_case 116 | 117 | # Local functions are PascalCase 118 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 119 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 120 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 121 | 122 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 123 | 124 | dotnet_naming_style.local_function_style.capitalization = pascal_case 125 | 126 | # By default, name items with PascalCase 127 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 128 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 129 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 130 | 131 | dotnet_naming_symbols.all_members.applicable_kinds = * 132 | 133 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 134 | 135 | ############################### 136 | # C# Coding Conventions # 137 | ############################### 138 | [*.cs] 139 | # var preferences 140 | csharp_style_var_for_built_in_types = true:silent 141 | csharp_style_var_when_type_is_apparent = true:silent 142 | csharp_style_var_elsewhere = true:silent 143 | # Expression-bodied members 144 | csharp_style_expression_bodied_methods = false:silent 145 | csharp_style_expression_bodied_constructors = false:silent 146 | csharp_style_expression_bodied_operators = false:silent 147 | csharp_style_expression_bodied_properties = true:silent 148 | csharp_style_expression_bodied_indexers = true:silent 149 | csharp_style_expression_bodied_accessors = true:silent 150 | # Pattern matching preferences 151 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 152 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 153 | # Null-checking preferences 154 | csharp_style_throw_expression = true:suggestion 155 | csharp_style_conditional_delegate_call = true:suggestion 156 | # Modifier preferences 157 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 158 | # Expression-level preferences 159 | csharp_prefer_braces = true:silent 160 | csharp_style_deconstructed_variable_declaration = true:suggestion 161 | csharp_prefer_simple_default_expression = true:suggestion 162 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 163 | csharp_style_inlined_variable_declaration = true:suggestion 164 | 165 | ############################### 166 | # C# Formatting Rules # 167 | ############################### 168 | # New line preferences 169 | csharp_new_line_before_open_brace = all 170 | csharp_new_line_before_else = true 171 | csharp_new_line_before_catch = true 172 | csharp_new_line_before_finally = true 173 | csharp_new_line_before_members_in_object_initializers = true 174 | csharp_new_line_before_members_in_anonymous_types = true 175 | csharp_new_line_between_query_expression_clauses = true 176 | # Indentation preferences 177 | csharp_indent_case_contents = true 178 | csharp_indent_switch_labels = true 179 | csharp_indent_labels = flush_left 180 | # Space preferences 181 | csharp_space_after_cast = false 182 | csharp_space_after_keywords_in_control_flow_statements = true 183 | csharp_space_between_method_call_parameter_list_parentheses = false 184 | csharp_space_between_method_declaration_parameter_list_parentheses = false 185 | csharp_space_between_parentheses = false 186 | csharp_space_before_colon_in_inheritance_clause = true 187 | csharp_space_after_colon_in_inheritance_clause = true 188 | csharp_space_around_binary_operators = before_and_after 189 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 190 | csharp_space_between_method_call_name_and_opening_parenthesis = false 191 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 192 | # Wrapping preferences 193 | csharp_preserve_single_line_statements = true 194 | csharp_preserve_single_line_blocks = true 195 | -------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "category": "Metadata", 4 | "guid": "0c136f8a-ff77-4f2b-ade5-13462cae6216", 5 | "name": "КиноПоиск", 6 | "overview": "Информация о фильмах и сериалах с КиноПоиска", 7 | "owner": "skrashevich", 8 | "description": "Загружает рейтинг, описания, актёров, трейлеры и т.д. с сайта КиноПоиск. Может потребоваться зарегистрировать свой ApiToken, см. информацию в параметрах плагина. Для точного распознавания рекомендуется указывать id фильма с сайта КиноПоиск в имени файла в формате kp-12345 или kp12345. Подробнее см. https://github.com/LinFor/jellyfin-plugin-kinopoisk/blob/master/README.md\n", 9 | "imageUrl": "https://kinopoisk.userecho.com/s/attachments/28876/0/1/25f8c0315e6ccb2aa6c2642e48f2c9e9.png", 10 | "versions": [ 11 | { 12 | "version": "10.9.0.1", 13 | "checksum": "4c0a0922dab5930781164d665a4b39da", 14 | "changelog": "new release", 15 | "name": "КиноПоиск", 16 | "targetAbi": "10.9.0", 17 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/v10-9/dist/kinopoisk/kinopoisk_10.9.0.1.zip", 18 | "timestamp": "2024-05-17T16:02:16Z" 19 | }, 20 | { 21 | "version": "10.9.0.0", 22 | "checksum": "46f29865d6ddb8e66f577ffd93cb8a55", 23 | "changelog": "new release", 24 | "name": "КиноПоиск", 25 | "targetAbi": "10.9.0", 26 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.9.0.0.zip", 27 | "timestamp": "2024-05-15T15:11:12Z" 28 | }, 29 | { 30 | "version": "10.8.9.3", 31 | "checksum": "b884a366e1da7e826267e8f13a3c1a09", 32 | "changelog": "new release", 33 | "name": "КиноПоиск", 34 | "targetAbi": "10.8.8.0", 35 | "sourceUrl": "https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.8.9.3.zip", 36 | "timestamp": "2023-06-14T22:56:46Z" 37 | }, 38 | { 39 | "version": "10.8.9.2", 40 | "checksum": "8cbefa9f1aa03dc054d0d8a83f49eb6b", 41 | "changelog": "new release", 42 | "name": "КиноПоиск", 43 | "targetAbi": "10.8.8.0", 44 | "sourceUrl": "https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.8.9.2.zip", 45 | "timestamp": "2023-02-08T16:42:11Z" 46 | }, 47 | { 48 | "version": "10.8.9.1", 49 | "checksum": "d15934ff981be4d7df861cd35a862971", 50 | "changelog": "new release", 51 | "name": "КиноПоиск", 52 | "targetAbi": "10.8.8.0", 53 | "sourceUrl": "https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.8.9.1.zip", 54 | "timestamp": "2023-02-08T16:10:12Z" 55 | }, 56 | { 57 | "version": "10.8.9.1", 58 | "checksum": "3790c3375057977320a77fe004f7c17f", 59 | "changelog": "new release", 60 | "targetAbi": "10.8.0", 61 | "sourceUrl": "https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.8.9.1.zip", 62 | "timestamp": "2023-02-08T15:37:42Z" 63 | }, 64 | { 65 | "version": "10.7.5.6", 66 | "changelog": "Update to .NET 7\n", 67 | "targetAbi": "10.7.0", 68 | "sourceUrl": "https://raw.githubusercontent.com/skrashevich/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.7.5.6.zip", 69 | "checksum": "60310678f450ca7f0da3811642ae2bc4", 70 | "timestamp": "2023-01-22T03:34:00Z" 71 | }, 72 | { 73 | "version": "10.7.5.4", 74 | "changelog": "Search fixed in case of null premiere date\n", 75 | "targetAbi": "10.7.0", 76 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.7.5.4.zip", 77 | "checksum": "2f48bd383e298da975e93a7f84fe3bc1", 78 | "timestamp": "2021-10-17T15:57:00Z" 79 | }, 80 | { 81 | "version": "10.7.5.3", 82 | "changelog": "Results parsing fixes due to upstream service changes (api 2.2)\n", 83 | "targetAbi": "10.7.0", 84 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.7.5.3.zip", 85 | "checksum": "43930ff3db6b6938dcbc23297c53c746", 86 | "timestamp": "2021-09-27T12:09:43Z" 87 | }, 88 | { 89 | "version": "10.7.5.2", 90 | "changelog": "Results parsing fixes due to upstream service changes\n", 91 | "targetAbi": "10.7.0", 92 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.7.5.2.zip", 93 | "checksum": "e35c1942fb8ceda1d351d7307174fab2", 94 | "timestamp": "2021-08-10T16:00:08Z" 95 | }, 96 | { 97 | "version": "10.7.5.1", 98 | "changelog": "Results parsing fixes\n", 99 | "targetAbi": "10.7.0", 100 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.7.5.1.zip", 101 | "checksum": "c41d06954e6c00d8b51f11ac11180cb3", 102 | "timestamp": "2021-06-22T09:27:10Z" 103 | }, 104 | { 105 | "version": "10.7.5.0", 106 | "changelog": "Short: Huge improvement in auto-identifying of movies. Rus: Значительно улучшено авто-привязка фильмов при добавлении в медиатеку. Пытаюсь найти идентификатор кинопоиска по паттерну kp-12345 или kp12345 в имени файла/папки. Пытаюсь найти единственное совпадение на кинопоиске по имени + году. Пытаюсь перебрать все совпадения по имени + сравнение IMDB ID.\n", 107 | "targetAbi": "10.7.0", 108 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.7.5.0.zip", 109 | "checksum": "ea7eabac0fc4d1891529616a16a02897", 110 | "timestamp": "2021-05-23T15:30:18Z" 111 | }, 112 | { 113 | "version": "10.7.0.5", 114 | "changelog": "Added trailers filtering: jellyfin-web can only play youtube trailers, not kinopoisk-hosted\n", 115 | "targetAbi": "10.7.0", 116 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.7.0.5.zip", 117 | "checksum": "6bdb97ee1e862b0b5657f7b510040148", 118 | "timestamp": "2021-05-22T23:01:09Z" 119 | }, 120 | { 121 | "version": "10.7.0.4", 122 | "changelog": "Added person info, person image, trailers fetching\n", 123 | "targetAbi": "10.7.0", 124 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.7.0.4.zip", 125 | "checksum": "28d4972fe0f5e193a8b3051ce5240ffe", 126 | "timestamp": "2021-05-22T22:24:49Z" 127 | }, 128 | { 129 | "version": "10.7.0.3", 130 | "changelog": "Don't provide empty poster Added kinopoisk hyperlink in people info\n", 131 | "targetAbi": "10.7.0", 132 | "sourceUrl": "https://raw.githubusercontent.com/LinFor/jellyfin-plugin-kinopoisk/master/dist/kinopoisk/kinopoisk_10.7.0.3.zip", 133 | "checksum": "d58cdb87e4e93b7ab1b22c14248841e2", 134 | "timestamp": "2021-05-21T16:00:03Z" 135 | }, 136 | { 137 | "checksum": "8d77d5f4de0bdf0abbce76cf8e5a251d", 138 | "changelog": "Fixed rating parsing", 139 | "name": "КиноПоиск", 140 | "targetAbi": "10.7.0", 141 | "runtimes": "netframework,netcore", 142 | "sourceUrl": "https://github.com/LinFor/jellyfin-plugin-kinopoisk/raw/master/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.7.0.2.zip", 143 | "filename": "jellyfin-plugin-kinopoisk_10.7.0.2.zip", 144 | "timestamp": "2021-04-06 18:12:00", 145 | "version": "10.7.0.2" 146 | }, 147 | { 148 | "checksum": "9051c982f7846af34249ed9969012c17", 149 | "changelog": "Fixed some actor professions parsing", 150 | "name": "КиноПоиск", 151 | "targetAbi": "10.7.0", 152 | "runtimes": "netframework,netcore", 153 | "sourceUrl": "https://github.com/LinFor/jellyfin-plugin-kinopoisk/raw/master/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.7.0.1.zip", 154 | "filename": "jellyfin-plugin-kinopoisk_10.7.0.1.zip", 155 | "timestamp": "2021-03-18 13:31:00", 156 | "version": "10.7.0.1" 157 | }, 158 | { 159 | "checksum": "ec7cfeff6a88f2427057d1bb6fb150b6", 160 | "changelog": "Jellyfin v10.7 compat", 161 | "name": "КиноПоиск", 162 | "targetAbi": "10.7.0", 163 | "runtimes": "netframework,netcore", 164 | "sourceUrl": "https://github.com/LinFor/jellyfin-plugin-kinopoisk/raw/master/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.7.0.0.zip", 165 | "filename": "jellyfin-plugin-kinopoisk_10.7.0.0.zip", 166 | "timestamp": "2021-03-16 11:35:00", 167 | "version": "10.7.0.0" 168 | }, 169 | { 170 | "checksum": "50674e215597995b96da17b49ee7ad62", 171 | "changelog": "fix NRE in person provider", 172 | "name": "КиноПоиск", 173 | "targetAbi": "10.6.0", 174 | "runtimes": "netframework,netcore", 175 | "sourceUrl": "https://github.com/LinFor/jellyfin-plugin-kinopoisk/raw/master/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.6.0.2.zip", 176 | "filename": "jellyfin-plugin-kinopoisk_10.6.0.2.zip", 177 | "timestamp": "2020-07-09 13:00:00", 178 | "version": "10.6.0.2" 179 | }, 180 | { 181 | "checksum": "a877cde0df7d70266fdb9362b72884c1", 182 | "changelog": "new release", 183 | "name": "КиноПоиск", 184 | "targetAbi": "10.6.0", 185 | "runtimes": "netframework,netcore", 186 | "sourceUrl": "https://github.com/LinFor/jellyfin-plugin-kinopoisk/raw/master/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.6.0.1.zip", 187 | "filename": "jellyfin-plugin-kinopoisk_10.6.0.1.zip", 188 | "timestamp": "2020-07-07 19:00:00", 189 | "version": "10.6.0.1" 190 | }, 191 | { 192 | "checksum": "8f43d3943f5f60293c6fb0ccbe560096", 193 | "changelog": "new release", 194 | "name": "КиноПоиск", 195 | "targetAbi": "10.6.0", 196 | "runtimes": "netframework,netcore", 197 | "sourceUrl": "https://github.com/LinFor/jellyfin-plugin-kinopoisk/raw/master/dist/kinopoisk/jellyfin-plugin-kinopoisk_10.6.0.0.zip", 198 | "filename": "jellyfin-plugin-kinopoisk_10.6.0.0.zip", 199 | "timestamp": "2020-07-07 10:00:00", 200 | "version": "10.6.0.0" 201 | } 202 | ] 203 | } 204 | ] 205 | -------------------------------------------------------------------------------- /src/Jellyfin.Plugin.Kinopoisk/ApiModelExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using KinopoiskUnofficialInfo.ApiClient; 6 | using MediaBrowser.Controller.Entities; 7 | using MediaBrowser.Controller.Entities.Movies; 8 | using MediaBrowser.Controller.Entities.TV; 9 | using MediaBrowser.Model.Entities; 10 | using MediaBrowser.Model.Providers; 11 | using Microsoft.Extensions.Logging; 12 | using Jellyfin.Data.Enums; 13 | using Jellyfin.Extensions; 14 | 15 | namespace Jellyfin.Plugin.Kinopoisk 16 | { 17 | public static class ApiModelExtensions 18 | { 19 | public static RemoteSearchResult ToRemoteSearchResult(this Film src) 20 | { 21 | if (src is null) 22 | return null; 23 | 24 | var res = new RemoteSearchResult() { 25 | Name = src.GetLocalName(), 26 | ImageUrl = src.PosterUrl, 27 | PremiereDate = src.GetPremiereDate(), 28 | Overview = src.Description, 29 | SearchProviderName = Constants.ProviderName 30 | }; 31 | res.SetProviderId(Constants.ProviderId, Convert.ToString(src.KinopoiskId)); 32 | 33 | return res; 34 | } 35 | 36 | public static IEnumerable ToRemoteSearchResults(this FilmSearchResponse src, ILogger logger) 37 | { 38 | if (src?.Films is null) 39 | return Enumerable.Empty(); 40 | 41 | return src.Films 42 | .Select(s => s.ToRemoteSearchResult(logger)) 43 | .Where(s => s != null); 44 | } 45 | 46 | public static RemoteSearchResult ToRemoteSearchResult(this FilmSearchResponse_films src, ILogger logger) 47 | { 48 | try { 49 | if (src is null) 50 | return null; 51 | 52 | var res = new RemoteSearchResult() { 53 | Name = src.GetLocalName(), 54 | ImageUrl = src.PosterUrl, 55 | PremiereDate = src.GetPremiereDate(), 56 | Overview = src.Description, 57 | SearchProviderName = Constants.ProviderName 58 | }; 59 | res.SetProviderId(Constants.ProviderId, Convert.ToString(src.FilmId)); 60 | 61 | return res; 62 | } 63 | catch (Exception e) { 64 | logger.LogError(e, "Exception during parse"); 65 | return null; 66 | } 67 | } 68 | 69 | public static Series ToSeries(this Film src) 70 | { 71 | if (src is null) 72 | return null; 73 | 74 | var res = new Series(); 75 | 76 | FillCommonFilmInfo(src, res); 77 | 78 | // res.EndDate = src.Data.GetEndDate(); 79 | // res.Status = src.Data.IsContinuing() 80 | // ? SeriesStatus.Continuing 81 | // : SeriesStatus.Ended; 82 | 83 | return res; 84 | } 85 | 86 | public static Movie ToMovie(this Film src) 87 | { 88 | if (src is null) 89 | return null; 90 | 91 | var res = new Movie(); 92 | 93 | FillCommonFilmInfo(src, res); 94 | 95 | return res; 96 | } 97 | 98 | private static void FillCommonFilmInfo(Film src, BaseItem dst) 99 | { 100 | dst.SetProviderId(Constants.ProviderId, Convert.ToString(src.KinopoiskId)); 101 | dst.Name = src.GetLocalName(); 102 | dst.OriginalTitle = src.GetOriginalNameIfNotSame(); 103 | dst.PremiereDate = src.GetPremiereDate(); 104 | if (!string.IsNullOrWhiteSpace(src.Slogan)) 105 | dst.Tagline = src.Slogan; 106 | dst.Overview = src.Description; 107 | if (src.Countries != null) 108 | dst.ProductionLocations = src.Countries.Select(c => c.Country1).ToArray(); 109 | if (src.Genres != null) 110 | foreach(var genre in src.Genres.Select(c => c.Genre1)) 111 | dst.AddGenre(genre); 112 | if (!string.IsNullOrEmpty(src.RatingAgeLimits)) 113 | dst.OfficialRating = $"{src.RatingAgeLimits}+"; 114 | else 115 | dst.OfficialRating = src.RatingMpaa; 116 | 117 | dst.CommunityRating = (float)src.RatingKinopoisk; 118 | if (dst.CommunityRating < 0.1) 119 | dst.CommunityRating = (float)src.RatingImdb; 120 | if (dst.CommunityRating < 0.1) 121 | dst.CommunityRating = null; 122 | dst.CriticRating = src.GetCriticRatingAsTenPointBased(); 123 | 124 | if (!string.IsNullOrWhiteSpace(src.ImdbId)) 125 | dst.SetProviderId(MetadataProvider.Imdb, src.ImdbId); 126 | } 127 | 128 | public static float? GetCriticRatingAsTenPointBased(this Film src) 129 | { 130 | if (src is null) 131 | return null; 132 | 133 | if (src.RatingRfCritics > 0.0) 134 | return (float)src.RatingRfCritics; 135 | 136 | if (src.RatingFilmCritics > 0.0) 137 | return (float)src.RatingFilmCritics; 138 | 139 | return null; 140 | } 141 | 142 | public static IEnumerable ToRemoteImageInfos(this Film src) 143 | { 144 | var res = Enumerable.Empty(); 145 | if (src is null) 146 | return res; 147 | 148 | if (src?.PosterUrl != null) 149 | { 150 | var mainPoster = new RemoteImageInfo(){ 151 | Type = ImageType.Primary, 152 | Url = src.PosterUrl, 153 | Language = Constants.ProviderMetadataLanguage, 154 | ProviderName = Constants.ProviderName 155 | }; 156 | res = res.Concat(Enumerable.Repeat(mainPoster, 1)); 157 | } 158 | 159 | // if (src.Images != null) 160 | // { 161 | // if (src.Images.Posters != null) 162 | // res = res.Concat(src.Images.Posters.ToRemoteImageInfos(ImageType.Primary)); 163 | // if (src.Images.Backdrops != null) 164 | // res = res.Concat(src.Images.Backdrops.ToRemoteImageInfos(ImageType.Backdrop)); 165 | // } 166 | 167 | return res; 168 | } 169 | 170 | // public static IEnumerable ToRemoteImageInfos(this IEnumerable src, ImageType imageType) 171 | // { 172 | // return src.Select(s => s.ToRemoteImageInfo(imageType)) 173 | // .Where(s => s != null); 174 | // } 175 | 176 | // public static RemoteImageInfo ToRemoteImageInfo(this Images_posters src, ImageType imageType) 177 | // { 178 | // if (src is null) 179 | // return null; 180 | 181 | // return new RemoteImageInfo(){ 182 | // Type = imageType, 183 | // Url = src.Url, 184 | // Language = src.Language, 185 | // Height = src.Height, 186 | // Width = src.Width, 187 | // ProviderName = Constants.ProviderName 188 | // }; 189 | // } 190 | 191 | public static IReadOnlyList ToMediaUrls(this VideoResponse src) 192 | { 193 | if (src is null || src.Items is null || src.Items.Count < 1) 194 | return null; 195 | 196 | return src.Items.Select(t => t.ToMediaUrl()) 197 | .Where(mu => mu != null) 198 | .ToList(); 199 | } 200 | 201 | public static MediaUrl ToMediaUrl(this VideoResponse_items src) { 202 | if (src is null || !VideoResponse_itemsSite.YOUTUBE.Equals(src.Site)) 203 | return null; 204 | 205 | return new MediaUrl 206 | { 207 | Name = src.Name, 208 | Url = src.Url.SanitizeYoutubeLink() 209 | }; 210 | } 211 | 212 | public static string SanitizeYoutubeLink(this string src) 213 | { 214 | // Jellyfin web currently recognizes only https://www.youtube.com/watch?v=xxx links 215 | return src 216 | .Replace("http://", "https://") 217 | .Replace("https://youtu.be/", "https://www.youtube.com/watch?v=") 218 | .Replace("https://www.youtube.com/v/", "https://www.youtube.com/watch?v="); 219 | } 220 | 221 | public static RemoteImageInfo ToRemoteImageInfo(this PersonResponse src) 222 | { 223 | if (src is null || string.IsNullOrEmpty(src.PosterUrl)) 224 | return null; 225 | 226 | return new RemoteImageInfo(){ 227 | Type = ImageType.Primary, 228 | Url = src.PosterUrl, 229 | ProviderName = Constants.ProviderName 230 | }; 231 | } 232 | 233 | public static PersonInfo ToPersonInfo(this StaffResponse src) 234 | { 235 | if (src is null) 236 | return null; 237 | 238 | var res = new PersonInfo() 239 | { 240 | Name = src.NameRu, 241 | ImageUrl = src.PosterUrl, 242 | Role = src.ProfessionText ?? null, 243 | Type = src.ProfessionKey.ToPersonType() 244 | }; 245 | if (string.IsNullOrWhiteSpace(res.Name)) 246 | res.Name = src.NameEn ?? string.Empty; 247 | if (src.AdditionalProperties.TryGetValue("description", out var description)) 248 | res.Role = description as string; 249 | 250 | res.SetProviderId(Constants.ProviderId, Convert.ToString(src.StaffId)); 251 | 252 | return res; 253 | } 254 | 255 | public static IEnumerable ToPersonInfos(this ICollection src) 256 | { 257 | var res = src.Select(s => s.ToPersonInfo()) 258 | .Where(s => s != null) 259 | .ToArray(); 260 | 261 | var i = 0; 262 | foreach(var item in res) 263 | item.SortOrder = ++i; 264 | 265 | return res; 266 | } 267 | 268 | public static PersonKind ToPersonType(this StaffResponseProfessionKey src) 269 | { 270 | return src switch 271 | { 272 | StaffResponseProfessionKey.ACTOR => PersonKind.Actor, 273 | StaffResponseProfessionKey.DIRECTOR or StaffResponseProfessionKey.VOICE_DIRECTOR or StaffResponseProfessionKey.OPERATOR => PersonKind.Director, 274 | StaffResponseProfessionKey.WRITER => PersonKind.Writer, 275 | StaffResponseProfessionKey.COMPOSER => PersonKind.Composer, 276 | StaffResponseProfessionKey.PRODUCER or StaffResponseProfessionKey.PRODUCER_USSR => PersonKind.Producer, 277 | StaffResponseProfessionKey.TRANSLATOR => PersonKind.Translator, 278 | StaffResponseProfessionKey.EDITOR => PersonKind.Editor, 279 | _ => PersonKind.Unknown, 280 | }; 281 | } 282 | 283 | public static DateTime? ParseDate(this string src){ 284 | if (src == null) 285 | return null; 286 | 287 | if (DateTime.TryParseExact(src, "o", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var res)) 288 | return res; 289 | 290 | return null; 291 | } 292 | 293 | public static DateTime? GetPremiereDate(this Film src) 294 | { 295 | // var res = src.IsRussianSpokenOriginated() 296 | // ? src.PremiereRu.ParseDate() 297 | // : src.PremiereWorld.ParseDate(); 298 | // if (src.PremiereRu.ParseDate() < res) 299 | // res = src.PremiereRu.ParseDate(); 300 | // if (src.PremiereWorld.ParseDate() < res) 301 | // res = src.PremiereWorld.ParseDate(); 302 | // if (src.PremiereDigital.ParseDate() < res) 303 | // res = src.PremiereDigital.ParseDate(); 304 | // if (src.PremiereDvd.ParseDate() < res) 305 | // res = src.PremiereDvd.ParseDate(); 306 | // if (src.PremiereBluRay.ParseDate() < res) 307 | // res = src.PremiereBluRay.ParseDate(); 308 | 309 | // if (res.HasValue) 310 | // return res; 311 | 312 | if (src.Year > 1900) 313 | return new DateTime(src.Year, 1, 1); 314 | 315 | return null; 316 | } 317 | 318 | public static DateTime? GetPremiereDate(this FilmSearchResponse_films src) 319 | { 320 | var firstYear = GetFirstYear(src.Year); 321 | if (firstYear != null) 322 | return new DateTime(firstYear.Value, 1, 1); 323 | 324 | return null; 325 | } 326 | 327 | public static string GetLocalName(this Film src) 328 | { 329 | var res = src?.NameRu; 330 | if (string.IsNullOrWhiteSpace(res)) 331 | res = src?.NameOriginal; 332 | if (string.IsNullOrWhiteSpace(res)) 333 | res = src?.NameEn; 334 | return res; 335 | } 336 | 337 | public static string GetLocalName(this FilmSearchResponse_films src) 338 | { 339 | var res = src?.NameRu; 340 | if (string.IsNullOrWhiteSpace(res)) 341 | res = src?.NameEn; 342 | return res; 343 | } 344 | 345 | public static string GetOriginalName(this Film src) 346 | => src?.NameOriginal ?? 347 | (src.IsRussianSpokenOriginated() 348 | ? src?.NameRu 349 | : src?.NameEn); 350 | 351 | public static string GetOriginalNameIfNotSame(this Film src) 352 | { 353 | var localName = src.GetLocalName(); 354 | var originalName = src.GetOriginalName(); 355 | if (!string.IsNullOrWhiteSpace(originalName) && !string.Equals(localName, originalName)) 356 | return originalName; 357 | 358 | return string.Empty; 359 | } 360 | 361 | public static bool IsRussianSpokenOriginated(this Film src) 362 | => src?.Countries?.IsRussianSpokenOriginated() ?? false; 363 | 364 | public static bool IsRussianSpokenOriginated(this IEnumerable src) 365 | { 366 | if (src is null) 367 | return false; 368 | 369 | foreach(var country in src) 370 | switch(country.Country1) 371 | { 372 | case "Россия": 373 | return true; 374 | } 375 | 376 | return false; 377 | } 378 | 379 | public static int? GetFirstYear(string years) 380 | { 381 | if (string.IsNullOrWhiteSpace(years) || years.ToLower() == "null") 382 | return null; 383 | 384 | years = years.Trim(); 385 | 386 | if (int.TryParse(years, out var res)) 387 | return res; 388 | 389 | var i = 0; 390 | while (true) { 391 | if (i > 4) 392 | return null; 393 | if (!char.IsDigit(years[i])) 394 | break; 395 | i++; 396 | } 397 | 398 | return Convert.ToInt32(years.Substring(0, i)); 399 | } 400 | 401 | public static bool IsСontinuing(string years) 402 | => years?.EndsWith("-...") ?? false; 403 | 404 | public static int? GetLastYear(string years) 405 | { 406 | if (string.IsNullOrWhiteSpace(years)) 407 | return null; 408 | 409 | years = years.Trim(); 410 | 411 | if (int.TryParse(years, out var res)) 412 | return res; 413 | 414 | var i = 0; 415 | int startindex() => years.Length - 1 - i; 416 | while (true) { 417 | if (i > 4) 418 | return null; 419 | if (!char.IsDigit(years[startindex()])) 420 | { 421 | i--; 422 | break; 423 | } 424 | i++; 425 | } 426 | 427 | return i > 0 428 | ? (int?)Convert.ToInt32(years[startindex()..]) 429 | : null; 430 | } 431 | 432 | public static Person ToPerson(this PersonResponse src) 433 | { 434 | if (src is null) 435 | return null; 436 | 437 | var res = new Person() 438 | { 439 | Name = src.GetLocalName(), 440 | PremiereDate = src.Birthday.ParseDate(), 441 | EndDate = src.Death.ParseDate() 442 | }; 443 | 444 | if (!string.IsNullOrWhiteSpace(src.Birthplace)) 445 | res.ProductionLocations = new[] { src.Birthplace }; 446 | 447 | return res; 448 | } 449 | 450 | public static string GetLocalName(this PersonResponse src) 451 | { 452 | var res = src?.NameRu; 453 | if (string.IsNullOrWhiteSpace(res)) 454 | res = src?.NameEn; 455 | return res; 456 | } 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/master/script-library/docs/common.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] 11 | 12 | INSTALL_ZSH=${1:-"true"} 13 | USERNAME=${2:-"automatic"} 14 | USER_UID=${3:-"automatic"} 15 | USER_GID=${4:-"automatic"} 16 | UPGRADE_PACKAGES=${5:-"true"} 17 | INSTALL_OH_MYS=${6:-"true"} 18 | ADD_NON_FREE_PACKAGES=${7:-"false"} 19 | 20 | set -e 21 | 22 | if [ "$(id -u)" -ne 0 ]; then 23 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 24 | exit 1 25 | fi 26 | 27 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 28 | rm -f /etc/profile.d/00-restore-env.sh 29 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 30 | chmod +x /etc/profile.d/00-restore-env.sh 31 | 32 | # If in automatic mode, determine if a user already exists, if not use vscode 33 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 34 | USERNAME="" 35 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 36 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 37 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 38 | USERNAME=${CURRENT_USER} 39 | break 40 | fi 41 | done 42 | if [ "${USERNAME}" = "" ]; then 43 | USERNAME=vscode 44 | fi 45 | elif [ "${USERNAME}" = "none" ]; then 46 | USERNAME=root 47 | USER_UID=0 48 | USER_GID=0 49 | fi 50 | 51 | # Load markers to see which steps have already run 52 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 53 | if [ -f "${MARKER_FILE}" ]; then 54 | echo "Marker file found:" 55 | cat "${MARKER_FILE}" 56 | source "${MARKER_FILE}" 57 | fi 58 | 59 | # Ensure apt is in non-interactive to avoid prompts 60 | export DEBIAN_FRONTEND=noninteractive 61 | 62 | # Function to call apt-get if needed 63 | apt-get-update-if-needed() 64 | { 65 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 66 | echo "Running apt-get update..." 67 | apt-get update 68 | else 69 | echo "Skipping apt-get update." 70 | fi 71 | } 72 | 73 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 74 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 75 | 76 | PACKAGE_LIST="apt-utils \ 77 | git \ 78 | openssh-client \ 79 | gnupg2 \ 80 | iproute2 \ 81 | procps \ 82 | lsof \ 83 | htop \ 84 | net-tools \ 85 | psmisc \ 86 | curl \ 87 | wget \ 88 | rsync \ 89 | ca-certificates \ 90 | unzip \ 91 | zip \ 92 | nano \ 93 | vim-tiny \ 94 | less \ 95 | jq \ 96 | lsb-release \ 97 | apt-transport-https \ 98 | dialog \ 99 | libc6 \ 100 | libgcc1 \ 101 | libkrb5-3 \ 102 | libgssapi-krb5-2 \ 103 | libicu[0-9][0-9] \ 104 | liblttng-ust0 \ 105 | libstdc++6 \ 106 | zlib1g \ 107 | locales \ 108 | sudo \ 109 | ncdu \ 110 | man-db \ 111 | strace \ 112 | manpages \ 113 | manpages-dev \ 114 | init-system-helpers" 115 | 116 | # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian 117 | if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then 118 | CODENAME="$(cat /etc/os-release | grep -oE '^VERSION_CODENAME=.+$' | cut -d'=' -f2)" 119 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${CODENAME} main contrib non-free/" /etc/apt/sources.list 120 | sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${CODENAME} main contrib non-free/" /etc/apt/sources.list 121 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 122 | sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 123 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 124 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 125 | sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 126 | sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 127 | echo "Running apt-get update..." 128 | apt-get update 129 | PACKAGE_LIST="${PACKAGE_LIST} manpages-posix manpages-posix-dev" 130 | else 131 | apt-get-update-if-needed 132 | fi 133 | 134 | # Install libssl1.1 if available 135 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 136 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.1" 137 | fi 138 | 139 | # Install appropriate version of libssl1.0.x if available 140 | LIBSSL=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 141 | if [ "$(echo "$LIBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 142 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 143 | # Debian 9 144 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.2" 145 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 146 | # Ubuntu 18.04, 16.04, earlier 147 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.0" 148 | fi 149 | fi 150 | 151 | echo "Packages to verify are installed: ${PACKAGE_LIST}" 152 | apt-get -y install --no-install-recommends ${PACKAGE_LIST} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 153 | 154 | PACKAGES_ALREADY_INSTALLED="true" 155 | fi 156 | 157 | # Get to latest versions of all packages 158 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 159 | apt-get-update-if-needed 160 | apt-get -y upgrade --no-install-recommends 161 | apt-get autoremove -y 162 | fi 163 | 164 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 165 | # Common need for both applications and things like the agnoster ZSH theme. 166 | if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then 167 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 168 | locale-gen 169 | LOCALE_ALREADY_SET="true" 170 | fi 171 | 172 | # Create or update a non-root user to match UID/GID. 173 | if id -u ${USERNAME} > /dev/null 2>&1; then 174 | # User exists, update if needed 175 | if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -G $USERNAME)" ]; then 176 | groupmod --gid $USER_GID $USERNAME 177 | usermod --gid $USER_GID $USERNAME 178 | fi 179 | if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 180 | usermod --uid $USER_UID $USERNAME 181 | fi 182 | else 183 | # Create user 184 | if [ "${USER_GID}" = "automatic" ]; then 185 | groupadd $USERNAME 186 | else 187 | groupadd --gid $USER_GID $USERNAME 188 | fi 189 | if [ "${USER_UID}" = "automatic" ]; then 190 | useradd -s /bin/bash --gid $USERNAME -m $USERNAME 191 | else 192 | useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME 193 | fi 194 | fi 195 | 196 | # Add add sudo support for non-root user 197 | if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 198 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 199 | chmod 0440 /etc/sudoers.d/$USERNAME 200 | EXISTING_NON_ROOT_USER="${USERNAME}" 201 | fi 202 | 203 | # ** Shell customization section ** 204 | if [ "${USERNAME}" = "root" ]; then 205 | USER_RC_PATH="/root" 206 | else 207 | USER_RC_PATH="/home/${USERNAME}" 208 | fi 209 | 210 | # .bashrc/.zshrc snippet 211 | RC_SNIPPET="$(cat << 'EOF' 212 | 213 | if [ -z "${USER}" ]; then export USER=$(whoami); fi 214 | if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi 215 | 216 | # Display optional first run image specific notice if configured and terminal is interactive 217 | if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then 218 | if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then 219 | cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" 220 | elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then 221 | cat "/workspaces/.codespaces/shared/first-run-notice.txt" 222 | fi 223 | mkdir -p "$HOME/.config/vscode-dev-containers" 224 | # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it 225 | ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) 226 | fi 227 | 228 | EOF 229 | )" 230 | 231 | # code shim, it fallbacks to code-insiders if code is not available 232 | cat << 'EOF' > /usr/local/bin/code 233 | #!/bin/sh 234 | 235 | get_in_path_except_current() { 236 | which -a "$1" | grep -A1 "$0" | grep -v "$0" 237 | } 238 | 239 | code="$(get_in_path_except_current code)" 240 | 241 | if [ -n "$code" ]; then 242 | exec "$code" "$@" 243 | elif [ "$(command -v code-insiders)" ]; then 244 | exec code-insiders "$@" 245 | else 246 | echo "code or code-insiders is not installed" >&2 247 | exit 127 248 | fi 249 | EOF 250 | chmod +x /usr/local/bin/code 251 | 252 | # systemctl shim - tells people to use 'service' if systemd is not running 253 | cat << 'EOF' > /usr/local/bin/systemctl 254 | #!/bin/sh 255 | set -e 256 | if [ -d "/run/systemd/system" ]; then 257 | exec /bin/systemctl/systemctl "$@" 258 | else 259 | echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services intead. e.g.: \n\nservice --status-all' 260 | fi 261 | EOF 262 | chmod +x /usr/local/bin/systemctl 263 | 264 | # Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme 265 | CODESPACES_BASH="$(cat \ 266 | <<'EOF' 267 | 268 | # Codespaces bash prompt theme 269 | __bash_prompt() { 270 | local userpart='`export XIT=$? \ 271 | && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ 272 | && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' 273 | local gitbranch='`\ 274 | export BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); \ 275 | if [ "${BRANCH}" = "HEAD" ]; then \ 276 | export BRANCH=$(git describe --contains --all HEAD 2>/dev/null); \ 277 | fi; \ 278 | if [ "${BRANCH}" != "" ]; then \ 279 | echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ 280 | && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ 281 | echo -n " \[\033[1;33m\]✗"; \ 282 | fi \ 283 | && echo -n "\[\033[0;36m\]) "; \ 284 | fi`' 285 | local lightblue='\[\033[1;34m\]' 286 | local removecolor='\[\033[0m\]' 287 | PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " 288 | unset -f __bash_prompt 289 | } 290 | __bash_prompt 291 | 292 | EOF 293 | )" 294 | CODESPACES_ZSH="$(cat \ 295 | <<'EOF' 296 | __zsh_prompt() { 297 | local prompt_username 298 | if [ ! -z "${GITHUB_USER}" ]; then 299 | prompt_username="@${GITHUB_USER}" 300 | else 301 | prompt_username="%n" 302 | fi 303 | PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow 304 | PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd 305 | PROMPT+='$(git_prompt_info)%{$fg[white]%}$ %{$reset_color%}' # Git status 306 | unset -f __zsh_prompt 307 | } 308 | ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" 309 | ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " 310 | ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" 311 | ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" 312 | __zsh_prompt 313 | EOF 314 | )" 315 | 316 | # Add notice that Oh My Bash! has been removed from images and how to provide information on how to install manually 317 | OMB_README="$(cat \ 318 | <<'EOF' 319 | "Oh My Bash!" has been removed from this image in favor of a simple shell prompt. If you 320 | still wish to use it, remove "~/.oh-my-bash" and install it from: https://github.com/ohmybash/oh-my-bash 321 | You may also want to consider "Bash-it" as an alternative: https://github.com/bash-it/bash-it 322 | See here for infomation on adding it to your image or dotfiles: https://aka.ms/codespaces/omb-remove 323 | EOF 324 | )" 325 | OMB_STUB="$(cat \ 326 | <<'EOF' 327 | #!/usr/bin/env bash 328 | if [ -t 1 ]; then 329 | cat $HOME/.oh-my-bash/README.md 330 | fi 331 | EOF 332 | )" 333 | 334 | # Add RC snippet and custom bash prompt 335 | if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then 336 | echo "${RC_SNIPPET}" >> /etc/bash.bashrc 337 | echo "${CODESPACES_BASH}" >> "${USER_RC_PATH}/.bashrc" 338 | echo 'export PROMPT_DIRTRIM=4' >> "${USER_RC_PATH}/.bashrc" 339 | if [ "${USERNAME}" != "root" ]; then 340 | echo "${CODESPACES_BASH}" >> "/root/.bashrc" 341 | echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" 342 | fi 343 | chown ${USERNAME}:${USERNAME} "${USER_RC_PATH}/.bashrc" 344 | RC_SNIPPET_ALREADY_ADDED="true" 345 | fi 346 | 347 | # Add stub for Oh My Bash! 348 | if [ ! -d "${USER_RC_PATH}/.oh-my-bash}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then 349 | mkdir -p "${USER_RC_PATH}/.oh-my-bash" "/root/.oh-my-bash" 350 | echo "${OMB_README}" >> "${USER_RC_PATH}/.oh-my-bash/README.md" 351 | echo "${OMB_STUB}" >> "${USER_RC_PATH}/.oh-my-bash/oh-my-bash.sh" 352 | chmod +x "${USER_RC_PATH}/.oh-my-bash/oh-my-bash.sh" 353 | if [ "${USERNAME}" != "root" ]; then 354 | echo "${OMB_README}" >> "/root/.oh-my-bash/README.md" 355 | echo "${OMB_STUB}" >> "/root/.oh-my-bash/oh-my-bash.sh" 356 | chmod +x "/root/.oh-my-bash/oh-my-bash.sh" 357 | fi 358 | chown -R "${USERNAME}:${USERNAME}" "${USER_RC_PATH}/.oh-my-bash" 359 | fi 360 | 361 | # Optionally install and configure zsh and Oh My Zsh! 362 | if [ "${INSTALL_ZSH}" = "true" ]; then 363 | if ! type zsh > /dev/null 2>&1; then 364 | apt-get-update-if-needed 365 | apt-get install -y zsh 366 | fi 367 | if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 368 | echo "${RC_SNIPPET}" >> /etc/zsh/zshrc 369 | ZSH_ALREADY_INSTALLED="true" 370 | fi 371 | 372 | # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. 373 | # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. 374 | OH_MY_INSTALL_DIR="${USER_RC_PATH}/.oh-my-zsh" 375 | if [ ! -d "${OH_MY_INSTALL_DIR}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then 376 | TEMPLATE_PATH="${OH_MY_INSTALL_DIR}/templates/zshrc.zsh-template" 377 | USER_RC_FILE="${USER_RC_PATH}/.zshrc" 378 | umask g-w,o-w 379 | mkdir -p ${OH_MY_INSTALL_DIR} 380 | git clone --depth=1 \ 381 | -c core.eol=lf \ 382 | -c core.autocrlf=false \ 383 | -c fsck.zeroPaddedFilemode=ignore \ 384 | -c fetch.fsck.zeroPaddedFilemode=ignore \ 385 | -c receive.fsck.zeroPaddedFilemode=ignore \ 386 | "https://github.com/ohmyzsh/ohmyzsh" "${OH_MY_INSTALL_DIR}" 2>&1 387 | echo -e "$(cat "${TEMPLATE_PATH}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${USER_RC_FILE} 388 | sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${USER_RC_FILE} 389 | 390 | mkdir -p ${OH_MY_INSTALL_DIR}/custom/themes 391 | echo "${CODESPACES_ZSH}" > "${OH_MY_INSTALL_DIR}/custom/themes/codespaces.zsh-theme" 392 | # Shrink git while still enabling updates 393 | cd "${OH_MY_INSTALL_DIR}" 394 | git repack -a -d -f --depth=1 --window=1 395 | # Copy to non-root user if one is specified 396 | if [ "${USERNAME}" != "root" ]; then 397 | cp -rf "${USER_RC_FILE}" "${OH_MY_INSTALL_DIR}" /root 398 | chown -R ${USERNAME}:${USERNAME} "${USER_RC_PATH}" 399 | fi 400 | fi 401 | fi 402 | 403 | # Persist image metadata info, script if meta.env found in same directory 404 | META_INFO_SCRIPT="$(cat << 'EOF' 405 | #!/bin/sh 406 | . /usr/local/etc/vscode-dev-containers/meta.env 407 | 408 | # Minimal output 409 | if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then 410 | echo "${VERSION}" 411 | exit 0 412 | elif [ "$1" = "release" ]; then 413 | echo "${GIT_REPOSITORY_RELEASE}" 414 | exit 0 415 | elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then 416 | echo "${CONTENTS_URL}" 417 | exit 0 418 | fi 419 | 420 | #Full output 421 | echo 422 | echo "Development container image information" 423 | echo 424 | if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi 425 | if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi 426 | if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi 427 | if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi 428 | if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi 429 | if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi 430 | if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi 431 | echo 432 | EOF 433 | )" 434 | SCRIPT_DIR="$(cd $(dirname $0) && pwd)" 435 | if [ -f "${SCRIPT_DIR}/meta.env" ]; then 436 | mkdir -p /usr/local/etc/vscode-dev-containers/ 437 | cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env 438 | echo "${META_INFO_SCRIPT}" > /usr/local/bin/devcontainer-info 439 | chmod +x /usr/local/bin/devcontainer-info 440 | fi 441 | 442 | # Write marker file 443 | mkdir -p "$(dirname "${MARKER_FILE}")" 444 | echo -e "\ 445 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 446 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 447 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 448 | RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ 449 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 450 | 451 | echo "Done!" 452 | -------------------------------------------------------------------------------- /src/KinopoiskUnofficialInfo.ApiClient.Tests/cassettes/GetSingleFilm_ShouldParseProducerUssr_89540.yaml: -------------------------------------------------------------------------------- 1 | version: VCR.net 1.0.0 2 | httpInteractions: 3 | - request: 4 | uri: https://kinopoiskapiunofficial.tech/api/v1/staff?filmId=89540 5 | method: GET 6 | headers: 7 | Accept: 8 | - application/json 9 | X-API-KEY: 10 | - 85d30ae5-d875-4c5f-900d-8e37bb20625e 11 | response: 12 | status: 13 | code: 200 14 | message: '' 15 | headers: 16 | Server: 17 | - nginx/1.18.0 18 | - (Ubuntu) 19 | Date: 20 | - Mon, 27 Sep 2021 10:34:31 GMT 21 | Transfer-Encoding: 22 | - chunked 23 | Connection: 24 | - keep-alive 25 | Vary: 26 | - Origin 27 | - Access-Control-Request-Method 28 | - Access-Control-Request-Headers 29 | X-Content-Type-Options: 30 | - nosniff 31 | X-XSS-Protection: 32 | - 1; mode=block 33 | Cache-Control: 34 | - no-store, must-revalidate, no-cache, max-age=0 35 | Pragma: 36 | - no-cache 37 | X-Frame-Options: 38 | - DENY 39 | Content-Type: 40 | - application/json 41 | Expires: 42 | - 0 43 | body: 44 | encoding: '' 45 | string: |- 46 | [{"staffId":318031,"nameRu":"Татьяна Лиознова","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/318031.jpg","professionText":"Режиссеры","professionKey":"DIRECTOR"},{"staffId":101751,"nameRu":"Вячеслав Тихонов","nameEn":"","description":"Штирлиц","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/101751.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":261544,"nameRu":"Леонид Броневой","nameEn":"","description":"Мюллер","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/261544.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":542446,"nameRu":"Екатерина Градова","nameEn":"","description":"Кэт","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/542446.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":313381,"nameRu":"Ростислав Плятт","nameEn":"","description":"пастор Шлаг","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/313381.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":122798,"nameRu":"Олег Табаков","nameEn":"","description":"Шелленберг","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/122798.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":190105,"nameRu":"Евгений Евстигнеев","nameEn":"","description":"Плейшнер","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/190105.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":403046,"nameRu":"Отто Мелис","nameEn":"Otto Mellies","description":"Гельмут","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/403046.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":185644,"nameRu":"Леонид Куравлёв","nameEn":"","description":"Айсман","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/185644.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":301199,"nameRu":"Константин Желдин","nameEn":"","description":"Холтофф","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/301199.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":675527,"nameRu":"Михаил Жарковский","nameEn":"","description":"Кальтенбруннер","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/675527.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":284241,"nameRu":"Николай Прокопович","nameEn":"","description":"Гиммлер","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/284241.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":174346,"nameRu":"Юрий Визбор","nameEn":"","description":"Борман","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/174346.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":191598,"nameRu":"Светлана Светличная","nameEn":"","description":"Габи Набель","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/191598.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":327385,"nameRu":"Эмилия Мильтон","nameEn":"","description":"фрау Заурих","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/327385.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":273090,"nameRu":"Николай Волков мл.","nameEn":"","description":"Эрвин","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/273090.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":261607,"nameRu":"Лев Дуров","nameEn":"","description":"Клаус","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/261607.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":256968,"nameRu":"Василий Лановой","nameEn":"","description":"генерал Вольф","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/256968.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":295050,"nameRu":"Ольга Сошникова","nameEn":"","description":"Барбара","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/295050.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":324141,"nameRu":"Николай Гриценко","nameEn":"","description":"генерал в вагоне","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/324141.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":313269,"nameRu":"Элеонора Шашкова","nameEn":"","description":"жена Исаева","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/313269.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":334610,"nameRu":"Лаврентий Масоха","nameEn":"","description":"Шольц","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/334610.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":290617,"nameRu":"Григорий Лямпе","nameEn":"","description":"физик Рунге","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/290617.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":284833,"nameRu":"Юрий Катин-Ярцев","nameEn":"","description":"астроном","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/284833.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":316932,"nameRu":"Андро Кобаладзе","nameEn":"","description":"Сталин","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/316932.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":321316,"nameRu":"Петр Чернов","nameEn":"","description":"Громов","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/321316.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":285821,"nameRu":"Ян Янакиев","nameEn":"","description":"Дольман","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/285821.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":273374,"nameRu":"Вячеслав Шалевич","nameEn":"","description":"Аллен Даллес","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/273374.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":320142,"nameRu":"Алексей Эйбоженко","nameEn":"","description":"Гюсман","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/320142.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":238567,"nameRu":"Валентин Гафт","nameEn":"","description":"Геверниц","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/238567.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":198154,"nameRu":"Владлен Давыдов","nameEn":"","description":"сотрудник Даллеса","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/198154.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":101756,"nameRu":"Инна Ульянова","nameEn":"","description":"девица в баре","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/101756.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":304332,"nameRu":"Владимир Кенигсон","nameEn":"","description":"Краузе","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/304332.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":290118,"nameRu":"Станислав Коренев","nameEn":"","description":"секретарь Кальтенбруннера","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/290118.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":567131,"nameRu":"Юрий Соковнин","nameEn":"","description":"шофер Бормана","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/567131.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":190115,"nameRu":"Ефим Копелян","nameEn":"","description":"текст от автора, озвучка","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/190115.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":84671,"nameRu":"Олег Федоров","nameEn":"","description":"немецкий солдат","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/84671.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":330061,"nameRu":"Фриц Диц","nameEn":"Fritz Diez","description":"Гитлер","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/330061.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":322148,"nameRu":"Владимир Козел","nameEn":"","description":"кюре","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/322148.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":37409,"nameRu":"Вадим Лобанов","nameEn":"","description":"страховой агент, шпион","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/37409.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":270882,"nameRu":"Паул Буткевич","nameEn":"","description":"связник","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/270882.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":342872,"nameRu":"Алексей Добронравов","nameEn":"","description":"сторож коттеджа Штирлица","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/342872.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":185646,"nameRu":"Алексей Сафонов","nameEn":"","description":"Рольф","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/185646.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":290610,"nameRu":"Виктор Щеглов","nameEn":"","description":"страховой агент","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/290610.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":269897,"nameRu":"Владимир Смирнов","nameEn":"","description":"хозяин явочной квартиры","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/269897.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":294729,"nameRu":"Валентин Белоногов","nameEn":"","description":"немецкий солдат, в титрах не указан","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/294729.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":325814,"nameRu":"Рудольф Панков","nameEn":"","description":"«Одноглазый»","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/325814.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":1083019,"nameRu":"Михаил Ремизов","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/1083019.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":1671771,"nameRu":"Владимир Рудый","nameEn":"","description":"Александр Коваленко","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/1671771.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":188868,"nameRu":"Зинаида Воркуль","nameEn":"","description":"вторая сестра в приюте","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/188868.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":320184,"nameRu":"Манефа Соболевская","nameEn":"","description":"первая сестра в приюте","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/320184.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":36393,"nameRu":"Евгений Лазарев","nameEn":"","description":"Емельянов","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/36393.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":333193,"nameRu":"Евгений Кузнецов","nameEn":"","description":"Крюгер","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/333193.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":317976,"nameRu":"Евгений Гуров","nameEn":"","description":"хозяин птичьего магазина","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/317976.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":269896,"nameRu":"Владислав Ковальков","nameEn":"","description":"эксперт","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/269896.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":1693303,"nameRu":"Вильгельм Бурмайер","nameEn":"Wilhelm Burmeier","description":"Геринг","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/1693303.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":185649,"nameRu":"Владлен Паулус","nameEn":"","description":"советник немецкого посольства","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/185649.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":344927,"nameRu":"Виктор Рождественский","nameEn":"","description":"адъютант","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/344927.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":543498,"nameRu":"Нина Делекторская","nameEn":"","description":"библиотекарь","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/543498.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":351013,"nameRu":"Клеон Протасов","nameEn":"","description":"стенографист Сталина","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/351013.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":231266,"nameRu":"Мария Миронова","nameEn":"","description":"новорожденный сын Кэт","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/231266.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":543413,"nameRu":"Юрий Багинян","nameEn":"","description":"адъютант Геринга","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/543413.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":174131,"nameRu":"Евгений Перов","nameEn":"","description":"хозяин ресторана","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/174131.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":315754,"nameRu":"Николай Симкин","nameEn":"","description":"последний комендант Берлина","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/315754.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":301262,"nameRu":"Евгений Новиков","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/301262.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":2376890,"nameRu":"Б. Андреев","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/2376890.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":196175,"nameRu":"Эдуард Изотов","nameEn":"","description":"адъютант Гитлера","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/196175.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":334748,"nameRu":"Владимир Бутенко","nameEn":"","description":"Бургдорф","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/334748.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":2332173,"nameRu":"Александр Крюков","nameEn":"","description":"Питер","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/2332173.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":2685697,"nameRu":"Юрий Заев","nameEn":"","description":"Биттнер","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/2685697.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":328660,"nameRu":"Алексей Бояршинов","nameEn":"","description":"Шпеер","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/328660.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":1950390,"nameRu":"Геннадий Петров","nameEn":"","description":"Васильев","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/1950390.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":3257355,"nameRu":"Иван Мочалов","nameEn":"","description":"в титрах не указан","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/3257355.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":552309,"nameRu":"Игорь Гусев","nameEn":"","description":"адъютант Бормана","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/552309.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":256844,"nameRu":"Александр Яковлев","nameEn":"","description":"охранник американского посольства","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/256844.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":4140207,"nameRu":"Антонина Маркова","nameEn":"","description":"медсестра","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/4140207.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":324206,"nameRu":"Тигран Давыдов","nameEn":"","description":"заключенный-уголовник","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/324206.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":595199,"nameRu":"Семён Бардин","nameEn":"","description":"посетитель ресторана, в титрах не указан","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/595199.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":174298,"nameRu":"Михаил Бочаров","nameEn":"","description":"полицейский, в тирах не указан","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/174298.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":346605,"nameRu":"Борис Буткеев","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/346605.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":281308,"nameRu":"Наталья Дрожжина","nameEn":"","description":"официантка","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/281308.jpg","professionText":"Актеры","professionKey":"ACTOR"},{"staffId":2331955,"nameRu":"Ефим Лебединский","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/2331955.jpg","professionText":"Директора фильма","professionKey":"PRODUCER_USSR"},{"staffId":185628,"nameRu":"Юлиан Семенов","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/185628.jpg","professionText":"Сценаристы","professionKey":"WRITER"},{"staffId":313275,"nameRu":"Петр Катаев","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/313275.jpg","professionText":"Операторы","professionKey":"OPERATOR"},{"staffId":261721,"nameRu":"Микаэл Таривердиев","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/261721.jpg","professionText":"Композиторы","professionKey":"COMPOSER"},{"staffId":2003890,"nameRu":"Борис Дуленков","nameEn":"","description":"постановщик","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/2003890.jpg","professionText":"Художники","professionKey":"DESIGN"},{"staffId":1038027,"nameRu":"Феликс Ростоцкий","nameEn":"","description":"постановщик","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/1038027.jpg","professionText":"Художники","professionKey":"DESIGN"},{"staffId":1997246,"nameRu":"Мариам Быховская","nameEn":"","description":"по костюмам","posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/1997246.jpg","professionText":"Художники","professionKey":"DESIGN"},{"staffId":1997244,"nameRu":"Ксения Блинова","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/1997244.jpg","professionText":"Монтажеры","professionKey":"EDITOR"},{"staffId":315808,"nameRu":"Ида Дорофеева","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/315808.jpg","professionText":"Монтажеры","professionKey":"EDITOR"},{"staffId":2008126,"nameRu":"В. Потапова","nameEn":"","description":null,"posterUrl":"https://kinopoiskapiunofficial.tech/images/actor_posters/kp/2008126.jpg","professionText":"Монтажеры","professionKey":"EDITOR"}] 47 | httpVersion: 1.1 48 | --------------------------------------------------------------------------------