From 812514861ace85d5968f1fa131c4f738f7f2c137 Mon Sep 17 00:00:00 2001 From: softworkz <softworkz@hotmail.com> Date: Thu, 25 Aug 2016 03:59:08 +0200 Subject: [PATCH] TV Maze Provider - Initial check in --- .../ExternalId.cs | 75 ++ .../Globals.cs | 16 + ...MediaBrowser.Plugins.TvMazeProvider.csproj | 112 +++ .../Properties/AssemblyInfo.cs | 23 + .../TvMaze/Models/MazeCastCredit.cs | 27 + .../TvMaze/Models/MazeCastMember.cs | 28 + .../TvMaze/Models/MazeChannel.cs | 21 + .../TvMaze/Models/MazeCountry.cs | 21 + .../TvMaze/Models/MazeCrewCredit.cs | 28 + .../TvMaze/Models/MazeEpisode.cs | 59 ++ .../TvMaze/Models/MazeExternals.cs | 21 + .../TvMaze/Models/MazeHuman.cs | 33 + .../TvMaze/Models/MazeImage.cs | 26 + .../TvMaze/Models/MazeLink.cs | 19 + .../TvMaze/Models/MazeLinks.cs | 13 + .../TvMaze/Models/MazeRating.cs | 13 + .../TvMaze/Models/MazeSearchContainer.cs | 25 + .../TvMaze/Models/MazeSeason.cs | 59 ++ .../TvMaze/Models/MazeSeries.cs | 101 +++ .../TvMaze/TvMazeAdapter.cs | 194 +++++ .../TvMaze/TvMazeEpisodeProvider.cs | 189 +++++ .../TvMaze/TvMazeSeasonImageProvider.cs | 137 ++++ .../TvMaze/TvMazeSeasonProvider.cs | 155 ++++ .../TvMaze/TvMazeSeriesProvider.cs | 702 ++++++++++++++++++ .../TvMazePlugin.cs | 28 + .../packages.config | 8 + MediaBrowser.Plugins.sln | 14 +- 27 files changed, 2145 insertions(+), 2 deletions(-) create mode 100644 MediaBrowser.Plugins.TvMazeProvider/ExternalId.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/Globals.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/MediaBrowser.Plugins.TvMazeProvider.csproj create mode 100644 MediaBrowser.Plugins.TvMazeProvider/Properties/AssemblyInfo.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCastCredit.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCastMember.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeChannel.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCountry.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCrewCredit.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeEpisode.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeExternals.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeHuman.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeImage.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeLink.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeLinks.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeRating.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSearchContainer.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSeason.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSeries.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeAdapter.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeEpisodeProvider.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeasonImageProvider.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeasonProvider.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeriesProvider.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/TvMazePlugin.cs create mode 100644 MediaBrowser.Plugins.TvMazeProvider/packages.config diff --git a/MediaBrowser.Plugins.TvMazeProvider/ExternalId.cs b/MediaBrowser.Plugins.TvMazeProvider/ExternalId.cs new file mode 100644 index 00000000..10b399b6 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/ExternalId.cs @@ -0,0 +1,75 @@ +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Providers.TV.TvMaze; + +namespace MediaBrowser.Plugins.TvMazeProvider +{ + public class TvMazeExternalId : IExternalId + { + public string Name + { + get { return "TV Maze Series"; } + } + + public string Key + { + get { return MetadataProviders.TvMaze.ToString(); } + } + + public string UrlFormatString + { + get { return "http://www.tvmaze.com/shows/{0}"; } + } + + public bool Supports(Model.Entities.IHasProviderIds item) + { + return item is Series; + } + } + + public class TvMazeEpisodeExternalId : IExternalId + { + public string Name + { + get { return "TV Maze Episode"; } + } + + public string Key + { + get { return MetadataProviders.TvMaze.ToString(); } + } + + public string UrlFormatString + { + get { return "http://www.tvmaze.com/episodes/{0}"; } + } + + public bool Supports(Model.Entities.IHasProviderIds item) + { + return item is Episode; + } + } + + public class TvMazeSeasonExternalId : IExternalId + { + public string Name + { + get { return "TV Maze Season"; } + } + + public string Key + { + get { return MetadataProviders.TvMaze.ToString(); } + } + + public string UrlFormatString + { + get { return "http://www.tvmaze.com/seasons/{0}/season"; } + } + + public bool Supports(Model.Entities.IHasProviderIds item) + { + return item is Season; + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/Globals.cs b/MediaBrowser.Plugins.TvMazeProvider/Globals.cs new file mode 100644 index 00000000..00d05330 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/Globals.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV.TvMaze +{ + public enum MetadataProviders + { + TvMaze, + Tvdb, + TvRage, + Imdb + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/MediaBrowser.Plugins.TvMazeProvider.csproj b/MediaBrowser.Plugins.TvMazeProvider/MediaBrowser.Plugins.TvMazeProvider.csproj new file mode 100644 index 00000000..6f329cb1 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/MediaBrowser.Plugins.TvMazeProvider.csproj @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProjectGuid>{5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>MediaBrowser.Plugins.TvMazeProvider</RootNamespace> + <AssemblyName>MediaBrowser.Plugins.TvMazeProvider</AssemblyName> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> + <RestorePackages>true</RestorePackages> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <ItemGroup> + <Reference Include="CommonIO, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\CommonIO.1.0.0.9\lib\net45\CommonIO.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Interfaces.IO"> + <HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="MediaBrowser.Common, Version=3.1.6052.21679, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\MediaBrowser.Common.3.0.654\lib\net45\MediaBrowser.Common.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="MediaBrowser.Controller, Version=3.1.6052.21678, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\MediaBrowser.Server.Core.3.0.654\lib\net45\MediaBrowser.Controller.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="MediaBrowser.Model, Version=3.1.6052.21679, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\MediaBrowser.Common.3.0.654\lib\net45\MediaBrowser.Model.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Patterns.Logging, Version=1.0.5494.41209, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="System.Web" /> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Data.DataSetExtensions" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="..\SharedVersion.cs"> + <Link>Properties\SharedVersion.cs</Link> + </Compile> + <Compile Include="Globals.cs" /> + <Compile Include="TvMazePlugin.cs" /> + <Compile Include="ExternalId.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="TvMaze\Models\MazeCastCredit.cs" /> + <Compile Include="TvMaze\Models\MazeCastMember.cs" /> + <Compile Include="TvMaze\Models\MazeChannel.cs" /> + <Compile Include="TvMaze\Models\MazeCountry.cs" /> + <Compile Include="TvMaze\Models\MazeCrewCredit.cs" /> + <Compile Include="TvMaze\Models\MazeEpisode.cs" /> + <Compile Include="TvMaze\Models\MazeExternals.cs" /> + <Compile Include="TvMaze\Models\MazeHuman.cs" /> + <Compile Include="TvMaze\Models\MazeImage.cs" /> + <Compile Include="TvMaze\Models\MazeLink.cs" /> + <Compile Include="TvMaze\Models\MazeLinks.cs" /> + <Compile Include="TvMaze\Models\MazeRating.cs" /> + <Compile Include="TvMaze\Models\MazeSearchContainer.cs" /> + <Compile Include="TvMaze\Models\MazeSeason.cs" /> + <Compile Include="TvMaze\Models\MazeSeries.cs" /> + <Compile Include="TvMaze\TvMazeAdapter.cs" /> + <Compile Include="TvMaze\TvMazeEpisodeProvider.cs" /> + <Compile Include="TvMaze\TvMazeSeasonImageProvider.cs" /> + <Compile Include="TvMaze\TvMazeSeasonProvider.cs" /> + <Compile Include="TvMaze\TvMazeSeriesProvider.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="packages.config" /> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" /> + <PropertyGroup> + <PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\..\MediaBrowser\ProgramData-Server\Plugins\" /y</PostBuildEvent> + </PropertyGroup> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> +</Project> \ No newline at end of file diff --git a/MediaBrowser.Plugins.TvMazeProvider/Properties/AssemblyInfo.cs b/MediaBrowser.Plugins.TvMazeProvider/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a375b76e --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/Properties/AssemblyInfo.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MediaBrowser.Plugins.TvMazeProvider")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("softworkz")] +[assembly: AssemblyProduct("TV Maze Metadata Provider")] +[assembly: AssemblyCopyright("Copyright © Emby 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6C39CA29-6EC8-4E72-A471-9D23136ADAF3")] diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCastCredit.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCastCredit.cs new file mode 100644 index 00000000..3995febd --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCastCredit.cs @@ -0,0 +1,27 @@ +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Another show the Actor has been in. + /// </summary> + public class MazeCastCredit + { + /// <summary> + /// Accessible links for relevant Cast information. + /// </summary> + public CastLink _links { get; set; } + /// <summary> + /// Accessible links for relevant Cast information. + /// </summary> + public class CastLink + { + /// <summary> + /// Link to the Character Page that this actor plays in a particular show. + /// </summary> + public MazeLink character { get; set; } + /// <summary> + /// Link to the show that this actor stars in. + /// </summary> + public MazeLink show { get; set; } + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCastMember.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCastMember.cs new file mode 100644 index 00000000..18936076 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCastMember.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Information about a particular Actor. + /// </summary> + public class MazeCastMember + { + /// <summary> + /// Actor's information. + /// </summary> + public MazeHuman person { get; set; } + /// <summary> + /// Actors' Character information. + /// </summary> + public MazeHuman character { get; set; } + /// <summary> + /// Collection of all Shows the Actor has played a part in. + /// </summary> + public IReadOnlyCollection<MazeCastCredit> castCredit { get; set; } + /// <summary> + /// Collection of all shows the Actor has done some behind the scenes work in. + /// </summary> + public IReadOnlyCollection<MazeCrewCredit> crewCredit { get; set; } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeChannel.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeChannel.cs new file mode 100644 index 00000000..e344c1ef --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeChannel.cs @@ -0,0 +1,21 @@ +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Information about a particular Network or WebChannel. + /// </summary> + public class MazeChannel + { + /// <summary> + /// Channel ID (Network or WebChannel, check Required for Ambiguity.) + /// </summary> + public int id { get; set; } + /// <summary> + /// Name of the Channel. + /// </summary> + public string name { get; set; } + /// <summary> + /// Country the Network originates from. + /// </summary> + public MazeCountry country { get; set; } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCountry.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCountry.cs new file mode 100644 index 00000000..7e991108 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCountry.cs @@ -0,0 +1,21 @@ +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Information about a particular Country and Timezone information. + /// </summary> + public class MazeCountry + { + /// <summary> + /// Full Name of Country. + /// </summary> + public string name { get; set; } + /// <summary> + /// ISO 2 Letter Country Code. + /// </summary> + public string code { get; set; } + /// <summary> + /// Timezone this particular show is on. + /// </summary> + public string timezone { get; set; } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCrewCredit.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCrewCredit.cs new file mode 100644 index 00000000..2ab05856 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeCrewCredit.cs @@ -0,0 +1,28 @@ +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Another role the Actor has been in. + /// </summary> + public class MazeCrewCredit + { + /// <summary> + /// Type of Role. + /// </summary> + public string type { get; set; } + /// <summary> + /// Accessible links for relevant Cast information. + /// </summary> + public CrewLinks _links { get; set; } + + /// <summary> + /// Accessible links for relevant Cast information. + /// </summary> + public class CrewLinks + { + /// <summary> + /// Link to the show in which this role was performed. + /// </summary> + public MazeLink show { get; set; } + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeEpisode.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeEpisode.cs new file mode 100644 index 00000000..8253cf26 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeEpisode.cs @@ -0,0 +1,59 @@ +using System; + +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// A collection of Information about an Episode on TVMaze. + /// </summary> + public class MazeEpisode + { + /// <summary> + /// Unique TVMaze Episode Identifier. + /// </summary> + public uint id { get; set; } + /// <summary> + /// Url to this Episode's Page on the Website. + /// </summary> + public Uri url { get; set; } + /// <summary> + /// Name of the Episode. + /// </summary> + public string name { get; set; } + /// <summary> + /// Season Number of the Episode. + /// </summary> + public int? season { get; set; } + /// <summary> + /// Episode Number in Season for the Episode. + /// </summary> + public int? number { get; set; } + /// <summary> + /// The Day that the Episode was/is First Aired. + /// </summary> + public DateTime? airdate { get; set; } + /// <summary> + /// Specfic Timezone offset time, for the AirTime of the Episode. + /// </summary> + public DateTimeOffset? airstamp { get; set; } + /// <summary> + /// How many minutes the Episode ran for. + /// </summary> + public int? runtime { get; set; } + /// <summary> + /// Images of the Episode. + /// </summary> + public MazeImage image { get; set; } + /// <summary> + /// A small description of the events of the Episode. + /// </summary> + public string summary { get; set; } + /// <summary> + /// Link to it's page on the website. + /// </summary> + public MazeLinks _links { get; set; } + /// <summary> + /// The show that this Episode originates from. + /// </summary> + public MazeSeries show { get; set; } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeExternals.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeExternals.cs new file mode 100644 index 00000000..b808b0e0 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeExternals.cs @@ -0,0 +1,21 @@ +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Identifiers for the series on other scrapers. + /// </summary> + public class MazeExternals + { + /// <summary> + /// ID for TVRage. + /// </summary> + public uint? tvrage { get; set; } + /// <summary> + /// ID for TheTVDB. + /// </summary> + public uint? thetvdb { get; set; } + /// <summary> + /// ID for the imdb Scraper. + /// </summary> + public string imdb { get; set; } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeHuman.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeHuman.cs new file mode 100644 index 00000000..df55937c --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeHuman.cs @@ -0,0 +1,33 @@ +using System; + +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + public class MazeHuman + { + /// <summary> + /// Unique TVMaze Human Identifier. + /// </summary> + public uint id { get; set; } + /// <summary> + /// Url to this Human's Page on the Website. + /// </summary> + public Uri url { get; set; } + /// <summary> + /// Name of the Person/Character. + /// </summary> + public string name { get; set; } + /// <summary> + /// Images of the Human. + /// </summary> + public MazeImage image { get; set; } + /// <summary> + /// Link to itself on the Website. + /// </summary> + public MazeLinks _links { get; set; } + + public override string ToString() + { + return name; + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeImage.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeImage.cs new file mode 100644 index 00000000..6df84919 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeImage.cs @@ -0,0 +1,26 @@ +using System; + +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Holds links for Images of items. + /// </summary> + public class MazeImage + { + /// <summary> + /// Resized and compressed for faster transfer. + /// </summary> + public Uri medium { get; set; } + /// <summary> + /// Original, best Image Quality. + /// </summary> + public Uri original { get; set; } + + public override string ToString() + { + if (medium != null && original != null) return "Original and Medium Quality"; + else if (medium != null) return "Medium Quality only"; + else return "Original Quality only"; + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeLink.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeLink.cs new file mode 100644 index 00000000..720fb5ed --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeLink.cs @@ -0,0 +1,19 @@ +using System; + +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Link for Accessing Various pages on the Scaper's website. + /// </summary> + public class MazeLink + { + /// <summary> + /// A Link to a Page on the Site. + /// </summary> + public Uri href { get; set; } + public override string ToString() + { + return href.ToString(); + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeLinks.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeLinks.cs new file mode 100644 index 00000000..331f34f7 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeLinks.cs @@ -0,0 +1,13 @@ +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Base Links Class points to itself as its only link. + /// </summary> + public class MazeLinks + { + /// <summary> + /// Link to the Website's Page + /// </summary> + public MazeLink self { get; set; } + } +} \ No newline at end of file diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeRating.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeRating.cs new file mode 100644 index 00000000..ce5fac83 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeRating.cs @@ -0,0 +1,13 @@ +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// Ratings that are given to Shows. + /// </summary> + public class MazeRating + { + /// <summary> + /// Average Rating Value for the Show. + /// </summary> + public double? average { get; set; } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSearchContainer.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSearchContainer.cs new file mode 100644 index 00000000..685a54ac --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSearchContainer.cs @@ -0,0 +1,25 @@ +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// A DeSerializable container to display the Show result and score of how close to query it is. + /// </summary> + public class MazeSearchContainerShow + { + public MazeSeries show { get; set; } + /// <summary> + /// Score rank of how close result is to query. + /// </summary> + public double score { get; set; } + } + /// <summary> + /// A DeSerializable container to display the Person result and score of how close to query it is. + /// </summary> + public class MazeSearchContainerPerson + { + public MazeHuman person { get; set; } + /// <summary> + /// Score rank of how close result is to query. + /// </summary> + public double score { get; set; } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSeason.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSeason.cs new file mode 100644 index 00000000..1ed59ea3 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSeason.cs @@ -0,0 +1,59 @@ +using System; + +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// A collection of Information about a Season on TVMaze. + /// </summary> + public class MazeSeason + { + /// <summary> + /// Unique TVMaze Season Identifier. + /// </summary> + public uint id { get; set; } + /// <summary> + /// Url to this Seasons's Page on the Website. + /// </summary> + public Uri url { get; set; } + /// <summary> + /// Name of the Season. + /// </summary> + public string name { get; set; } + /// <summary> + /// Season Number. + /// </summary> + public int? number { get; set; } + /// <summary> + /// Number of episodes in this season. + /// </summary> + public int? episodeOrder { get; set; } + /// <summary> + /// The Day that the first Episode was/is First Aired. + /// </summary> + public DateTime? premiereDate { get; set; } + /// <summary> + /// The Day that the last Episode was/is First Aired. + /// </summary> + public DateTime? endDate { get; set; } + /// <summary> + /// Network of Show. + /// </summary> + public MazeChannel network { get; set; } + /// <summary> + /// WebChannel of show. + /// </summary> + public MazeChannel webChannel { get; set; } + /// <summary> + /// Images of the Season. + /// </summary> + public MazeImage image { get; set; } + /// <summary> + /// A small description of the events of the Episode. + /// </summary> + public string summary { get; set; } + /// <summary> + /// Link to it's page on the website. + /// </summary> + public MazeLinks _links { get; set; } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSeries.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSeries.cs new file mode 100644 index 00000000..7cb397d8 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/Models/MazeSeries.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV.TvMaze.Models +{ + /// <summary> + /// A collection of Information about a Series on TVMaze. + /// </summary> + public class MazeSeries + { + /// <summary> + /// Unique TVMaze Show Identifier (0 if Show can't be found). + /// </summary> + public uint id { get; set; } + /// <summary> + /// Url to this Show's Page on the Website. + /// </summary> + public Uri url { get; set; } + /// <summary> + /// Name of the Show. + /// </summary> + public string name { get; set; } + /// <summary> + /// Style of presentation of the Show. + /// </summary> + public string type { get; set; } + /// <summary> + /// Language the Show is spoken in. + /// </summary> + public string language { get; set; } + /// <summary> + /// Collection of Genres the Show has. + /// </summary> + public string[] genres { get; set; } + /// <summary> + /// Status of the Show (404 if no Result when scraping). + /// </summary> + public string status { get; set; } + /// <summary> + /// Images of the Episode. + /// </summary> + public int? runtime { get; set; } + /// <summary> + /// Specfic Date when the First Episode of the show aired. + /// </summary> + public DateTime? premiered { get; set; } + /// <summary> + /// Rating of the Show by the community. + /// </summary> + public MazeRating rating { get; set; } + /// <summary> + /// A Series Ranking system based on a combination of votes, follow counts and page views. + /// </summary> + public int weight { get; set; } + /// <summary> + /// Network of Show. + /// </summary> + public MazeChannel network { get; set; } + /// <summary> + /// WebChannel of show. + /// </summary> + public MazeChannel webChannel { get; set; } + /// <summary> + /// External ID's to other Scrapers. + /// </summary> + public MazeExternals externals { get; set; } + /// <summary> + /// Images of the Series. + /// </summary> + public MazeImage image { get; set; } + /// <summary> + /// A small description of the Series. + /// </summary> + public string summary { get; set; } + /// <summary> + /// Links to Itself, and the Next and Previous Episodes. + /// </summary> + public SeriesLinks _links { get; set; } + /// <summary> + /// Collection of Episodes in a Show. + /// </summary> + public IReadOnlyCollection<MazeEpisode> Episodes { get; set; } + /// <summary> + /// Collection of Actors in a Show. + /// </summary> + public IReadOnlyCollection<MazeCastMember> Cast { get; set; } + + public class SeriesLinks : MazeLinks + { + /// <summary> + /// Link to the Webpage for the Previous Episode in the Series. + /// </summary> + public MazeLink previousepisode { get; set; } + /// <summary> + /// Link to the Webpage for the Next Episode in the Series. + /// </summary> + public MazeLink nextepisode { get; set; } + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeAdapter.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeAdapter.cs new file mode 100644 index 00000000..acb28cbe --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeAdapter.cs @@ -0,0 +1,194 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.TV.TvMaze.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV.TvMaze +{ + class TvMazeAdapter + { + public static Series Convert(MazeSeries mazeSeries) + { + var series = new Series(); + + SetProviderIds(series, mazeSeries.externals, mazeSeries.id); + + series.Name = mazeSeries.name; + series.Genres = mazeSeries.genres.ToList(); + + // TODO: Do we have a Series property for original language? + //series = mazeSeries.language; + + if (mazeSeries.network != null && !string.IsNullOrWhiteSpace(mazeSeries.network.name)) + { + var networkName = mazeSeries.network.name; + if (mazeSeries.network.country != null && !string.IsNullOrWhiteSpace(mazeSeries.network.country.code)) + { + networkName = string.Format("{0} ({1})", mazeSeries.network.name, mazeSeries.network.country.code); + } + + series.Studios.Add(networkName); + } + + if (mazeSeries.premiered.HasValue) + { + series.PremiereDate = mazeSeries.premiered.Value; + series.ProductionYear = mazeSeries.premiered.Value.Year; + } + + if (mazeSeries.rating != null && mazeSeries.rating.average.HasValue) + { + series.CommunityRating = (float)mazeSeries.rating.average.Value; + } + + if (mazeSeries.runtime.HasValue) + { + series.RunTimeTicks = TimeSpan.FromMinutes(mazeSeries.runtime.Value).Ticks; + } + + switch (mazeSeries.status.ToLower()) + { + case "running": + series.Status = SeriesStatus.Continuing; + break; + case "ended": + series.Status = SeriesStatus.Ended; + break; + } + + series.Overview = StripHtml(mazeSeries.summary); + + series.HomePageUrl = mazeSeries.url.ToString(); + + return series; + } + + public static Episode Convert(MazeEpisode mazeEpisode) + { + var episode = new Episode(); + + episode.ProviderIds[MetadataProviders.TvMaze.ToString()] = mazeEpisode.id.ToString(); + + episode.Name = mazeEpisode.name; + + episode.IndexNumber = mazeEpisode.number; + episode.ParentIndexNumber = mazeEpisode.season; + + if (mazeEpisode.airdate.HasValue) + { + episode.PremiereDate = mazeEpisode.airdate.Value; + } + + if (mazeEpisode.runtime.HasValue) + { + episode.RunTimeTicks = TimeSpan.FromMinutes(mazeEpisode.runtime.Value).Ticks; + } + + episode.Overview = StripHtml(mazeEpisode.summary); + + return episode; + } + + public static Season Convert(MazeSeason mazeSeason) + { + var season = new Season(); + + season.ProviderIds[MetadataProviders.TvMaze.ToString()] = mazeSeason.id.ToString(); + + season.Name = mazeSeason.name; + + season.IndexNumber = mazeSeason.number; + + if (mazeSeason.network != null && !string.IsNullOrWhiteSpace(mazeSeason.network.name)) + { + var networkName = mazeSeason.network.name; + if (mazeSeason.network.country != null && !string.IsNullOrWhiteSpace(mazeSeason.network.country.code)) + { + networkName = string.Format("{0} ({1})", mazeSeason.network.name, mazeSeason.network.country.code); + } + + season.Studios.Add(networkName); + } + + if (mazeSeason.premiereDate.HasValue) + { + season.PremiereDate = mazeSeason.premiereDate.Value; + season.ProductionYear = mazeSeason.premiereDate.Value.Year; + } + + if (mazeSeason.endDate.HasValue) + { + season.EndDate = mazeSeason.endDate.Value; + } + + season.Overview = StripHtml(mazeSeason.summary); + + return season; + } + + public static PersonInfo Convert(MazeCastMember mazeMember) + { + var personInfo = new PersonInfo(); + + personInfo.ProviderIds[MetadataProviders.TvMaze.ToString()] = mazeMember.person.id.ToString(); + + personInfo.Name = mazeMember.person.name; + personInfo.Role = mazeMember.character.name; + personInfo.Type = PersonType.Actor; + + if (mazeMember.person.image != null && mazeMember.person.image.original != null) + { + personInfo.ImageUrl = mazeMember.person.image.original.ToString(); + } + + return personInfo; + } + + private static void SetProviderIds(BaseItem item, MazeExternals externals, uint mazeId) + { + item.ProviderIds[MetadataProviders.TvMaze.ToString()] = mazeId.ToString(); + + if (externals.thetvdb.HasValue) + { + item.ProviderIds[MetadataProviders.Tvdb.ToString()] = externals.thetvdb.Value.ToString(); + } + + if (externals.tvrage.HasValue) + { + item.ProviderIds[MetadataProviders.TvRage.ToString()] = externals.tvrage.Value.ToString(); + } + + if (!string.IsNullOrWhiteSpace(externals.imdb)) + { + item.ProviderIds[MetadataProviders.Imdb.ToString()] = externals.imdb; + } + } + + private static string StripHtml(string content) + { + var result = content.Replace("<br>", Environment.NewLine); + result = result.Replace("<p>", ""); + result = result.Replace("</p>", ""); + result = result.Replace("<i>", ""); + result = result.Replace("</i>", ""); + result = result.Replace("<b>", ""); + result = result.Replace("</b>", ""); + result = result.Replace("<li>", ""); + result = result.Replace("</li>", ""); + result = result.Replace("<ul>", ""); + result = result.Replace("</ul>", ""); + result = result.Replace("<div>", ""); + result = result.Replace("<br />", ""); + result = result.Replace("<br/>", ""); + result = result.Replace("<em>", ""); + result = result.Replace("<em/>", ""); + + return result; + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeEpisodeProvider.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeEpisodeProvider.cs new file mode 100644 index 00000000..539fbcb0 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeEpisodeProvider.cs @@ -0,0 +1,189 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using CommonIO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.TV.TvMaze.Models; + +namespace MediaBrowser.Providers.TV.TvMaze +{ + + /// <summary> + /// Class RemoteEpisodeProvider + /// </summary> + class TvMazeEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasItemChangeMonitor + { + internal static TvMazeEpisodeProvider Current; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; + + public TvMazeEpisodeProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager config, IHttpClient httpClient, ILogger logger) + { + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + _config = config; + _httpClient = httpClient; + _logger = logger; + Current = this; + } + + public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { + var list = new List<RemoteSearchResult>(); + + // The search query must provide an episode number + if (!searchInfo.IndexNumber.HasValue) + { + return list; + } + + if (TvMazeSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) + { + var seriesDataPath = await TvMazeSeriesProvider.Current.EnsureSeriesInfo(searchInfo.SeriesProviderIds, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + try + { + var metadataResult = FetchEpisodeData(searchInfo, seriesDataPath, cancellationToken); + + if (metadataResult.HasMetadata) + { + var item = metadataResult.Item; + + list.Add(new RemoteSearchResult + { + IndexNumber = item.IndexNumber, + Name = item.Name, + ParentIndexNumber = item.ParentIndexNumber, + PremiereDate = item.PremiereDate, + ProductionYear = item.ProductionYear, + ProviderIds = item.ProviderIds, + SearchProviderName = Name, + IndexNumberEnd = item.IndexNumberEnd + }); + } + } + catch (FileNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + catch (DirectoryNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + } + + return list; + } + + public string Name + { + get { return TvMazeSeriesProvider.Current.Name; } + } + + public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { + var result = new MetadataResult<Episode>(); + + if (TvMazeSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) && + (searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue)) + { + var seriesDataPath = await TvMazeSeriesProvider.Current.EnsureSeriesInfo(searchInfo.SeriesProviderIds, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + try + { + result = FetchEpisodeData(searchInfo, seriesDataPath, cancellationToken); + } + catch (FileNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + catch (DirectoryNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + } + else + { + _logger.Debug("No series identity found for {0}", searchInfo.Name); + } + + return result; + } + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) + { + // Only enable for virtual items + if (item.LocationType != LocationType.Virtual) + { + return false; + } + + var episode = (Episode)item; + var series = episode.Series; + + if (series != null && TvMazeSeriesProvider.IsValidSeries(series.ProviderIds)) + { + // Process images + var seriesDataPath = TvMazeSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, series.ProviderIds); + var seriesPath = TvMazeSeriesProvider.Current.GetSeriesPath(seriesDataPath); + + return _fileSystem.GetLastWriteTimeUtc(seriesPath) > item.DateLastRefreshed; + } + + return false; + } + + /// <summary> + /// Fetches the episode data. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Episode}.</returns> + private MetadataResult<Episode> FetchEpisodeData(EpisodeInfo id, string seriesDataPath, CancellationToken cancellationToken) + { + var episodeFileName = TvMazeSeriesProvider.Current.GetEpisodePath(seriesDataPath, id.ParentIndexNumber.Value, id.IndexNumber.Value); + + var mazeEpisode = _jsonSerializer.DeserializeFromFile<MazeEpisode>(episodeFileName); + var episode = TvMazeAdapter.Convert(mazeEpisode); + + var result = new MetadataResult<Episode>() + { + Item = episode, + HasMetadata = true + }; + + return result; + } + + public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = TvMazeSeriesProvider.Current.TvMazeResourcePool + }); + } + + public int Order { get { return 0; } } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeasonImageProvider.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeasonImageProvider.cs new file mode 100644 index 00000000..6a94fe50 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeasonImageProvider.cs @@ -0,0 +1,137 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using CommonIO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.TV.TvMaze.Models; + +namespace MediaBrowser.Providers.TV.TvMaze +{ + public class TvMazeSeasonImageProvider : IRemoteImageProvider, IHasOrder + { + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _jsonSerializer; + + public TvMazeSeasonImageProvider(IJsonSerializer jsonSerializer, IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; + } + + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return TvMazeSeriesProvider.Current.Name; } + } + + public bool Supports(IHasImages item) + { + return item is Season; + } + + public IEnumerable<ImageType> GetSupportedImages(IHasImages item) + { + return new List<ImageType> + { + ImageType.Primary + }; + } + + public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, CancellationToken cancellationToken) + { + var season = (Season)item; + var series = season.Series; + + + if (series != null && season.IndexNumber.HasValue && TvMazeSeriesProvider.IsValidSeries(series.ProviderIds)) + { + var seriesProviderIds = series.ProviderIds; + var seasonNumber = season.IndexNumber.Value; + + var seriesDataPath = await TvMazeSeriesProvider.Current.EnsureSeriesInfo(seriesProviderIds, series.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrWhiteSpace(seriesDataPath)) + { + var seasonFileName = TvMazeSeriesProvider.Current.GetSeasonPath(seriesDataPath, seasonNumber); + + + try + { + var mazeSeason = _jsonSerializer.DeserializeFromFile<MazeSeason>(seasonFileName); + return GetImages(mazeSeason); + } + catch (FileNotFoundException) + { + // No tv maze data yet. Don't blow up + } + catch (DirectoryNotFoundException) + { + // No tv maze data yet. Don't blow up + } + } + } + + return new RemoteImageInfo[] { }; + } + + private static IEnumerable<RemoteImageInfo> GetImages(MazeSeason mazeSeason) + { + var result = new List<RemoteImageInfo>(); + + if (mazeSeason.image != null && mazeSeason.image.original != null) + { + var imageInfo = new RemoteImageInfo + { + Url = mazeSeason.image.original.AbsoluteUri, + ProviderName = ProviderName, + Language = "en", + Type = ImageType.Primary + }; + + result.Add(imageInfo); + } + + return result; + } + + public int Order + { + get { return 0; } + } + + public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = TvMazeSeriesProvider.Current.TvMazeResourcePool + }); + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeasonProvider.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeasonProvider.cs new file mode 100644 index 00000000..7e47581a --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeasonProvider.cs @@ -0,0 +1,155 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using CommonIO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.TV.TvMaze.Models; + +namespace MediaBrowser.Providers.TV.TvMaze +{ + + /// <summary> + /// Class TvMazeSeasonProvider + /// </summary> + class TvMazeSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo>, IHasItemChangeMonitor + { + internal static TvMazeSeasonProvider Current; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; + + public TvMazeSeasonProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager config, IHttpClient httpClient, ILogger logger) + { + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + _config = config; + _httpClient = httpClient; + _logger = logger; + Current = this; + } + + public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken) + { + return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>()); + } + + public string Name + { + get { return TvMazeSeriesProvider.Current.Name; } + } + + public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo searchInfo, CancellationToken cancellationToken) + { + var result = new MetadataResult<Season>(); + + if (TvMazeSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) && + searchInfo.IndexNumber.HasValue) + { + ////result.QueriedById = true; + var seriesDataPath = await TvMazeSeriesProvider.Current.EnsureSeriesInfo(searchInfo.SeriesProviderIds, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + try + { + result = FetchSeasonData(searchInfo, seriesDataPath, cancellationToken); + } + catch (FileNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + catch (DirectoryNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + } + else + { + _logger.Debug("No series identity found for {0}", searchInfo.Name); + } + + return result; + } + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) + { + // Only enable for virtual items + if (item.LocationType != LocationType.Virtual) + { + return false; + } + + var episode = (Episode)item; + var series = episode.Series; + + if (series != null && TvMazeSeriesProvider.IsValidSeries(series.ProviderIds)) + { + // Process images + var seriesDataPath = TvMazeSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, series.ProviderIds); + var seriesPath = TvMazeSeriesProvider.Current.GetSeriesPath(seriesDataPath); + + return _fileSystem.GetLastWriteTimeUtc(seriesPath) > item.DateLastRefreshed; + } + + return false; + } + + /// <summary> + /// Fetches the season data. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{Season}.</returns> + private MetadataResult<Season> FetchSeasonData(SeasonInfo info, string seriesDataPath, CancellationToken cancellationToken) + { + var seasonFileName = TvMazeSeriesProvider.Current.GetSeasonPath(seriesDataPath, info.IndexNumber.Value); + + var mazeSeason = _jsonSerializer.DeserializeFromFile<MazeSeason>(seasonFileName); + var season = TvMazeAdapter.Convert(mazeSeason); + + if (string.IsNullOrEmpty(season.Name)) + { + season.Name = info.Name; + } + + if (!season.IndexNumber.HasValue) + { + season.IndexNumber = info.IndexNumber.Value; + } + + var result = new MetadataResult<Season>() + { + Item = season, + HasMetadata = true + }; + + return result; + } + + public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = TvMazeSeriesProvider.Current.TvMazeResourcePool + }); + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeriesProvider.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeriesProvider.cs new file mode 100644 index 00000000..f5138a2b --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMaze/TvMazeSeriesProvider.cs @@ -0,0 +1,702 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.TV.TvMaze.Models; + +namespace MediaBrowser.Providers.TV.TvMaze +{ + public class TvMazeSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder + { + internal readonly SemaphoreSlim TvMazeResourcePool = new SemaphoreSlim(2, 2); + internal static TvMazeSeriesProvider Current { get; private set; } + private readonly IJsonSerializer _jsonSerializer; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + + public TvMazeSeriesProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager) + { + _jsonSerializer = jsonSerializer; + _httpClient = httpClient; + _fileSystem = fileSystem; + _config = config; + _logger = logger; + _libraryManager = libraryManager; + Current = this; + } + + private const string SeriesSearchUrl = "http://api.tvmaze.com/search/shows?q={0}"; + private const string UrlSeriesData = "http://api.tvmaze.com/shows/{0}"; + private const string UrlSeriesEpisodes = "http://api.tvmaze.com/shows/{0}/episodes"; + private const string UrlSeriesSeasons = "http://api.tvmaze.com/shows/{0}/seasons"; + private const string UrlSeriesCast = "http://api.tvmaze.com/shows/{0}/cast"; + private const string UrlByRemoteId = "http://api.tvmaze.com/lookup/shows?{0}={1}"; + + public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) + { + if (IsValidSeries(searchInfo.ProviderIds)) + { + var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false); + + if (metadata.HasMetadata) + { + return new List<RemoteSearchResult> + { + new RemoteSearchResult + { + Name = metadata.Item.Name, + PremiereDate = metadata.Item.PremiereDate, + ProductionYear = metadata.Item.ProductionYear, + ProviderIds = metadata.Item.ProviderIds, + SearchProviderName = Name + } + }; + } + } + + return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + } + + public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken) + { + var result = new MetadataResult<Series>(); + + if (!IsValidSeries(itemId.ProviderIds)) + { + await Identify(itemId).ConfigureAwait(false); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (IsValidSeries(itemId.ProviderIds)) + { + await EnsureSeriesInfo(itemId.ProviderIds, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + result.Item = new Series(); + result.HasMetadata = true; + + FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken); + } + + return result; + } + + /// <summary> + /// Fetches the series data. + /// </summary> + /// <param name="result">The result.</param> + /// <param name="metadataLanguage">The metadata language.</param> + /// <param name="seriesProviderIds">The series provider ids.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{System.Boolean}.</returns> + private void FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken) + { + var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); + + var seriesPath = GetSeriesPath(seriesDataPath); + var castPath = GetCastPath(seriesDataPath); + + result.Item = FetchSeriesInfo(seriesPath, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + var series = result.Item; + + string id; + if (seriesProviderIds.TryGetValue(MetadataProviders.TvMaze.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + series.SetProviderId(MetadataProviders.TvMaze.ToString(), id); + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + series.SetProviderId(MetadataProviders.Tvdb.ToString(), id); + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + series.SetProviderId(MetadataProviders.Imdb.ToString(), id); + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.TvRage.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + series.SetProviderId(MetadataProviders.TvRage.ToString(), id); + } + + result.ResetPeople(); + + FetchCast(result, castPath); + } + + private async Task<string> GetSeriesByRemoteId(string id, MetadataProviders idType, CancellationToken cancellationToken) + { + var idTypeString = idType.ToString().ToLower(); + + if (idTypeString == "tvdb") + { + idTypeString = "thetvdb"; + } + + var url = string.Format(UrlByRemoteId, idTypeString, id); + + using (var response = await _httpClient.GetResponse(new HttpRequestOptions + { + Url = url, + ResourcePool = TvMazeResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + if (response.StatusCode == HttpStatusCode.OK) + { + var arr = response.ResponseUrl.Split('/'); + return arr[arr.Length - 1]; + } + } + + return null; + } + + internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds) + { + string id; + if (seriesProviderIds.TryGetValue(MetadataProviders.TvMaze.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + return true; + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + return true; + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + return true; + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.TvRage.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + return true; + } + + return false; + } + + private SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); + internal async Task<string> EnsureSeriesInfo(Dictionary<string, string> seriesProviderIds, string preferredMetadataLanguage, CancellationToken cancellationToken) + { + await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + string seriesId; + MetadataProviders idType; + + if (seriesProviderIds.TryGetValue(MetadataProviders.TvMaze.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) + { + idType = MetadataProviders.TvMaze; + } + else if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) + { + idType = MetadataProviders.Tvdb; + } + else if (seriesProviderIds.TryGetValue(MetadataProviders.TvRage.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) + { + idType = MetadataProviders.TvRage; + } + else if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) + { + idType = MetadataProviders.Imdb; + } + else + { + throw new ArgumentException("TvMazeSeriesProvider.EnsureSeriesInfos: Missing provider id"); + } + + if (idType != MetadataProviders.TvMaze) + { + seriesId = await GetSeriesByRemoteId(seriesId, idType, cancellationToken).ConfigureAwait(false); + seriesProviderIds[MetadataProviders.TvMaze.ToString()] = seriesId; + } + + if (!string.IsNullOrWhiteSpace(seriesId)) + { + var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); + + if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) + { + var url = string.Format(UrlSeriesData, seriesId); + + using (var resultStream = await _httpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvMazeResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + if (!_fileSystem.DirectoryExists(seriesDataPath)) + { + _fileSystem.CreateDirectory(seriesDataPath); + } + + var mazeSeries = _jsonSerializer.DeserializeFromStream<MazeSeries>(resultStream); + + if (mazeSeries.status == "404") + { + throw new Exception("TvMazeSeriesProvider: Series could not be found!"); + } + + // Delete existing files + DeleteCacheFiles(seriesDataPath); + + _jsonSerializer.SerializeToFile(mazeSeries, GetSeriesPath(seriesDataPath)); + } + + await DownloadEpisodes(seriesDataPath, seriesId, cancellationToken).ConfigureAwait(false); + await DownloadSeasons(seriesDataPath, seriesId, cancellationToken).ConfigureAwait(false); + await DownloadCast(seriesDataPath, seriesId, cancellationToken).ConfigureAwait(false); + } + + return seriesDataPath; + } + + return null; + } + finally + { + _ensureSemaphore.Release(); + } + } + + private bool IsCacheValid(string seriesDataPath, string preferredMetadataLanguage) + { + try + { + var seriesFilename = GetSeriesPath(seriesDataPath); + + if (!_fileSystem.FileExists(seriesFilename)) + { + return false; + } + + var fileInfo = _fileSystem.GetFileInfo(seriesFilename); + + const int cacheDays = 2; + + if (fileInfo == null || !fileInfo.Exists || (DateTime.UtcNow - fileInfo.LastWriteTimeUtc).TotalDays > cacheDays) + { + return false; + } + + return true; + } + catch (DirectoryNotFoundException) + { + return false; + } + catch (FileNotFoundException) + { + return false; + } + } + + /// <summary> + /// Finds the series. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="year">The year.</param> + /// <param name="language">The language.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{System.String}.</returns> + private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken) + { + var results = (await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false)).ToList(); + + if (results.Count == 0) + { + var parsedName = _libraryManager.ParseName(name); + var nameWithoutYear = parsedName.Name; + + if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase)) + { + results = (await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false)).ToList(); + } + } + + return results.Where(i => + { + if (year.HasValue && i.ProductionYear.HasValue) + { + // Allow one year tolerance + return Math.Abs(year.Value - i.ProductionYear.Value) <= 1; + } + + return true; + }); + } + + private async Task<IEnumerable<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken) + { + var url = string.Format(SeriesSearchUrl, WebUtility.UrlEncode(name)); + MazeSearchContainerShow[] mazeResultItems; + + using (var results = await _httpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvMazeResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + mazeResultItems = _jsonSerializer.DeserializeFromStream<MazeSearchContainerShow[]>(results); + } + + var searchResults = new List<RemoteSearchResult>(); + var comparableName = GetComparableName(name); + + foreach (var mazeResultItem in mazeResultItems) + { + var searchResult = new RemoteSearchResult + { + SearchProviderName = Name + }; + + var mazeSeries = mazeResultItem.show; + + searchResult.Name = mazeSeries.name; + searchResult.SetProviderId(MetadataProviders.TvMaze.ToString(), mazeSeries.id.ToString()); + + if (mazeSeries.externals != null && !string.IsNullOrWhiteSpace(mazeSeries.externals.imdb)) + { + searchResult.SetProviderId(MetadataProviders.Imdb.ToString(), mazeSeries.externals.imdb); + } + + if (mazeSeries.image != null && mazeSeries.image.original != null) + { + searchResult.ImageUrl = mazeSeries.image.original.ToString(); + } + + if (mazeSeries.premiered.HasValue) + { + searchResult.ProductionYear = mazeSeries.premiered.Value.Year; + } + + searchResults.Add(searchResult); + } + + if (searchResults.Count == 0) + { + _logger.Info("TV Maze Provider - Could not find " + name + ". Check name on tvmaze.com"); + } + + return searchResults; + } + + /// <summary> + /// The remove + /// </summary> + const string remove = "\"'!`?"; + /// <summary> + /// The spacers + /// </summary> + const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes) + + /// <summary> + /// Gets the name of the comparable. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>System.String.</returns> + internal static string GetComparableName(string name) + { + name = name.ToLower(); + name = name.Normalize(NormalizationForm.FormKD); + var sb = new StringBuilder(); + foreach (var c in name) + { + if ((int)c >= 0x2B0 && (int)c <= 0x0333) + { + // skip char modifier and diacritics + } + else if (remove.IndexOf(c) > -1) + { + // skip chars we are removing + } + else if (spacers.IndexOf(c) > -1) + { + sb.Append(" "); + } + else if (c == '&') + { + sb.Append(" and "); + } + else + { + sb.Append(c); + } + } + name = sb.ToString(); + name = name.Replace(", the", ""); + name = name.Replace("the ", " "); + name = name.Replace(" the ", " "); + + string prevName; + do + { + prevName = name; + name = name.Replace(" ", " "); + } while (name.Length != prevName.Length); + + return name.Trim(); + } + + private Series FetchSeriesInfo(string seriesJsonPath, CancellationToken cancellationToken) + { + var mazeSeries = _jsonSerializer.DeserializeFromFile<MazeSeries>(seriesJsonPath); + return TvMazeAdapter.Convert(mazeSeries); + } + + /// <summary>DS + /// Fetches the actors. + /// </summary> + /// <param name="result">The result.</param> + /// <param name="castJsonPath">The cast path.</param> + private void FetchCast(MetadataResult<Series> result, string castJsonPath) + { + var persons = _jsonSerializer.DeserializeFromFile<PersonInfo[]>(castJsonPath); + + foreach (var person in persons) + { + result.AddPerson(person); + } + } + + /// <summary> + /// Extracts info for each episode into invididual json files so that they can be easily accessed + /// </summary> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="seriesId">The tvmaze id.</param> + /// <param name="cancellationToken">The cancellationToken.</param> + /// <returns>Task.</returns> + private async Task DownloadEpisodes(string seriesDataPath, string seriesId, CancellationToken cancellationToken) + { + var url = string.Format(UrlSeriesEpisodes, seriesId); + + using (var resultStream = await _httpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvMazeResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + var mazeEpisodes = _jsonSerializer.DeserializeFromStream<MazeEpisode[]>(resultStream); + + foreach (var mazeEpisode in mazeEpisodes) + { + if (mazeEpisode.season.HasValue && mazeEpisode.number.HasValue) + { + var episodeFilename = GetEpisodePath(seriesDataPath, mazeEpisode.season.Value, mazeEpisode.number.Value); + _jsonSerializer.SerializeToFile(mazeEpisode, episodeFilename); + } + } + } + } + + /// <summary> + /// Extracts cast info for a series. + /// </summary> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="seriesId">The tvmaze id.</param> + /// <param name="cancellationToken">The cancellationToken.</param> + /// <returns>Task.</returns> + private async Task DownloadCast(string seriesDataPath, string seriesId, CancellationToken cancellationToken) + { + var url = string.Format(UrlSeriesCast, seriesId); + + using (var resultStream = await _httpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvMazeResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + var mazeCastMembers = _jsonSerializer.DeserializeFromStream<MazeCastMember[]>(resultStream); + + var persons = new List<PersonInfo>(); + + foreach (var mazeCastMember in mazeCastMembers) + { + var person = TvMazeAdapter.Convert(mazeCastMember); + persons.Add(person); + } + + var castPath = GetCastPath(seriesDataPath); + _jsonSerializer.SerializeToFile(persons.ToArray(), castPath); + } + } + + /// <summary> + /// Extracts info for each season into invididual json files so that they can be easily accessed + /// </summary> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="seriesId">The tvmaze id.</param> + /// <param name="cancellationToken">The cancellationToken.</param> + /// <returns>Task.</returns> + private async Task DownloadSeasons(string seriesDataPath, string seriesId, CancellationToken cancellationToken) + { + var url = string.Format(UrlSeriesSeasons, seriesId); + + using (var resultStream = await _httpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvMazeResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + var mazeSeasons = _jsonSerializer.DeserializeFromStream<MazeSeason[]>(resultStream); + + foreach (var mazeSeason in mazeSeasons) + { + if (mazeSeason.number.HasValue) + { + var seasonFilename = GetSeasonPath(seriesDataPath, mazeSeason.number.Value); + _jsonSerializer.SerializeToFile(mazeSeason, seasonFilename); + } + } + } + } + + /// <summary> + /// Gets the series data path. + /// </summary> + /// <param name="appPaths">The app paths.</param> + /// <param name="seriesProviderIds">The series provider ids.</param> + /// <returns>System.String.</returns> + internal static string GetSeriesDataPath(IApplicationPaths appPaths, Dictionary<string, string> seriesProviderIds) + { + string seriesId; + if (seriesProviderIds.TryGetValue(MetadataProviders.TvMaze.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) + { + var dataPath = Path.Combine(appPaths.CachePath, "tvmaze"); + + var seriesDataPath = Path.Combine(dataPath, seriesId); + + return seriesDataPath; + } + + return null; + } + + public string GetSeriesPath(string seriesDataPath) + { + var seriesFilename = "series.json"; + + return Path.Combine(seriesDataPath, seriesFilename); + } + + public string GetCastPath(string seriesDataPath) + { + var seriesFilename = "cast.json"; + + return Path.Combine(seriesDataPath, seriesFilename); + } + + public string GetEpisodePath(string seriesDataPath, int seasonNumber, int episodeNumber) + { + var episodeFilename = string.Format("episode-{0}-{1}.json", seasonNumber, episodeNumber); + + return Path.Combine(seriesDataPath, episodeFilename); + } + + public string GetSeasonPath(string seriesDataPath, int seasonNumber) + { + var seasonFilename = string.Format("season-{0}.json", seasonNumber); + + return Path.Combine(seriesDataPath, seasonFilename); + } + + private void DeleteCacheFiles(string path) + { + try + { + foreach (var file in _fileSystem.GetFilePaths(path, true).ToList()) + { + _fileSystem.DeleteFile(file); + } + } + catch (DirectoryNotFoundException) + { + // No biggie + } + } + + public string Name + { + get { return "TV Maze"; } + } + + public async Task Identify(SeriesInfo info) + { + if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProviders.TvMaze.ToString()))) + { + return; + } + + var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None).ConfigureAwait(false); + + var entry = srch.FirstOrDefault(); + + if (entry != null) + { + var id = entry.GetProviderId(MetadataProviders.TvMaze.ToString()); + info.SetProviderId(MetadataProviders.TvMaze.ToString(), id); + } + } + + public int Order + { + get + { + // After Omdb or TvDB + return 1; + } + } + + public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = TvMazeResourcePool + }); + } + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/TvMazePlugin.cs b/MediaBrowser.Plugins.TvMazeProvider/TvMazePlugin.cs new file mode 100644 index 00000000..625c9da8 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/TvMazePlugin.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Plugins.TvMazeProvider +{ + public class TvMazePlugin : BasePlugin<TvMazePluginConfiguration> + { + public TvMazePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) + { + } + + public override string Name + { + get { return "TV Maze Provider"; } + } + + public override string Description + { + get { return "Metadata provider for TV shows using data from www.tvmaze.com"; } + } + } + + public class TvMazePluginConfiguration : BasePluginConfiguration + { + } +} diff --git a/MediaBrowser.Plugins.TvMazeProvider/packages.config b/MediaBrowser.Plugins.TvMazeProvider/packages.config new file mode 100644 index 00000000..6ca5a844 --- /dev/null +++ b/MediaBrowser.Plugins.TvMazeProvider/packages.config @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="CommonIO" version="1.0.0.9" targetFramework="net45" /> + <package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" /> + <package id="MediaBrowser.Common" version="3.0.654" targetFramework="net45" /> + <package id="MediaBrowser.Server.Core" version="3.0.654" targetFramework="net45" /> + <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" /> +</packages> \ No newline at end of file diff --git a/MediaBrowser.Plugins.sln b/MediaBrowser.Plugins.sln index 1baa81f5..a0abc087 100644 --- a/MediaBrowser.Plugins.sln +++ b/MediaBrowser.Plugins.sln @@ -1,7 +1,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.40629.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Plugins.RottenTomatoes", "MediaBrowser.Plugins.RottenTomatoes\MediaBrowser.Plugins.RottenTomatoes.csproj", "{71BF266C-BFB0-4821-8909-FF21F95A4B49}" EndProject @@ -56,6 +56,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Plugins.ADEPro EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MovieOrganizer", "MovieOrganizer\MovieOrganizer.csproj", "{0449243E-8FBD-47DF-8E79-8FBA6E735D36}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Plugins.TvMazeProvider", "MediaBrowser.Plugins.TvMazeProvider\MediaBrowser.Plugins.TvMazeProvider.csproj", "{5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -208,6 +210,14 @@ Global {0449243E-8FBD-47DF-8E79-8FBA6E735D36}.Release|Any CPU.ActiveCfg = Release|Any CPU {0449243E-8FBD-47DF-8E79-8FBA6E735D36}.Release|Any CPU.Build.0 = Release|Any CPU {0449243E-8FBD-47DF-8E79-8FBA6E735D36}.Release|x86.ActiveCfg = Release|Any CPU + {5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}.Debug|x86.ActiveCfg = Debug|Any CPU + {5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}.Debug|x86.Build.0 = Debug|Any CPU + {5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}.Release|Any CPU.Build.0 = Release|Any CPU + {5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}.Release|x86.ActiveCfg = Release|Any CPU + {5BAAE6C6-4C7C-4B6F-A475-07AF0FEF8620}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE