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