Implemented series title searching

This commit is contained in:
Thomas Gillen 2013-10-27 18:58:37 +00:00
parent 6bef3a9f96
commit 3eab70eda9
14 changed files with 60119 additions and 0 deletions

View File

@ -0,0 +1,69 @@
using System;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Logging;
using Moq;
using NUnit.Framework;
namespace MediaBrowser.Plugins.AniDB.Tests
{
[TestFixture]
public class AniDbTitleMatcherTests
{
[Test]
public async Task LoadsTitles()
{
var paths = new Mock<IApplicationPaths>();
paths.Setup(p => p.DataPath).Returns("TestData");
var logger = new Mock<ILogger>();
var matcher = new AniDbTitleMatcher(paths.Object, logger.Object);
await matcher.Load();
Assert.That(matcher.IsLoaded, Is.True);
Assert.That(await matcher.FindSeries("CotS"), Is.EqualTo("1"));
Assert.That(await matcher.FindSeries("Crest of the Stars"), Is.EqualTo("1"));
Assert.That(await matcher.FindSeries("星界の紋章"), Is.EqualTo("1"));
Assert.That(await matcher.FindSeries("サザンアイズ"), Is.EqualTo("2"));
Assert.That(await matcher.FindSeries("3x3 Eyes"), Is.EqualTo("2"));
Assert.That(await matcher.FindSeries("Sazan Eyes"), Is.EqualTo("2"));
}
[Test]
public async Task LoadsOnFindIfNotLoaded()
{
var paths = new Mock<IApplicationPaths>();
paths.Setup(p => p.DataPath).Returns("TestData");
var logger = new Mock<ILogger>();
var matcher = new AniDbTitleMatcher(paths.Object, logger.Object);
Assert.That(matcher.IsLoaded, Is.False);
Assert.That(await matcher.FindSeries("CotS"), Is.EqualTo("1"));
Assert.That(await matcher.FindSeries("Crest of the Stars"), Is.EqualTo("1"));
Assert.That(await matcher.FindSeries("星界の紋章"), Is.EqualTo("1"));
Assert.That(await matcher.FindSeries("サザンアイズ"), Is.EqualTo("2"));
Assert.That(await matcher.FindSeries("3x3 Eyes"), Is.EqualTo("2"));
Assert.That(await matcher.FindSeries("Sazan Eyes"), Is.EqualTo("2"));
}
[Test]
public async Task ErrorLoggedIfTitlesFileMissing()
{
var paths = new Mock<IApplicationPaths>();
paths.Setup(p => p.DataPath).Returns("InvalidPath");
var logger = new Mock<ILogger>();
logger.Setup(l => l.ErrorException("Failed to load AniDB titles", It.IsAny<Exception>())).Verifiable();
var matcher = new AniDbTitleMatcher(paths.Object, logger.Object);
await matcher.Load();
logger.Verify();
}
}
}

View File

@ -0,0 +1,82 @@
<?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>{96568724-B901-4805-9B16-7BDD5142124B}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MediaBrowser.Plugins.AniDB.Tests</RootNamespace>
<AssemblyName>MediaBrowser.Plugins.AniDB.Tests</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</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="MediaBrowser.Common">
<HintPath>..\packages\MediaBrowser.Common.3.0.234\lib\net45\MediaBrowser.Common.dll</HintPath>
</Reference>
<Reference Include="MediaBrowser.Controller">
<HintPath>..\packages\MediaBrowser.Server.Core.3.0.234\lib\net45\MediaBrowser.Controller.dll</HintPath>
</Reference>
<Reference Include="MediaBrowser.Model">
<HintPath>..\packages\MediaBrowser.Common.3.0.234\lib\net45\MediaBrowser.Model.dll</HintPath>
</Reference>
<Reference Include="Moq">
<HintPath>..\packages\Moq.4.1.1309.1617\lib\net40\Moq.dll</HintPath>
</Reference>
<Reference Include="nunit.framework">
<HintPath>..\packages\NUnit.2.6.3\lib\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<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="AniDbTitleMatcherTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Plugins.AniDB\MediaBrowser.Plugins.AniDB.csproj">
<Project>{f68223a5-ee1e-444c-89f9-14092aab987d}</Project>
<Name>MediaBrowser.Plugins.AniDB</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="TestData\anidb\titles.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 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>

View File

@ -0,0 +1,36 @@
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.AniDB.Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MediaBrowser.Plugins.AniDB.Tests")]
[assembly: AssemblyCopyright("Copyright © 2013")]
[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("04fbf019-f8e0-47bc-b21e-d5e15a73e98c")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="MediaBrowser.Common" version="3.0.234" targetFramework="net45" />
<package id="MediaBrowser.Server.Core" version="3.0.234" targetFramework="net45" />
<package id="Moq" version="4.1.1309.1617" targetFramework="net45" />
<package id="NUnit" version="2.6.3" targetFramework="net45" />
</packages>

View File

@ -0,0 +1,26 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Plugins.AniDB", "MediaBrowser.Plugins.AniDB\MediaBrowser.Plugins.AniDB.csproj", "{F68223A5-EE1E-444C-89F9-14092AAB987D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Plugins.AniDB.Tests", "MediaBrowser.Plugins.AniDB.Tests\MediaBrowser.Plugins.AniDB.Tests.csproj", "{96568724-B901-4805-9B16-7BDD5142124B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F68223A5-EE1E-444C-89F9-14092AAB987D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F68223A5-EE1E-444C-89F9-14092AAB987D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F68223A5-EE1E-444C-89F9-14092AAB987D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F68223A5-EE1E-444C-89F9-14092AAB987D}.Release|Any CPU.Build.0 = Release|Any CPU
{96568724-B901-4805-9B16-7BDD5142124B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{96568724-B901-4805-9B16-7BDD5142124B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{96568724-B901-4805-9B16-7BDD5142124B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{96568724-B901-4805-9B16-7BDD5142124B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,84 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Logging;
namespace MediaBrowser.Plugins.AniDB
{
/// <summary>
/// The AniDbPrescanTask class is a library prescan task which loads series titles from AniDB.
/// </summary>
public class AniDbPrescanTask : ILibraryPrescanTask
{
/// <summary>
/// The URL for retrieving a list of all anime titles and their AniDB IDs.
/// </summary>
private const string TitlesUrl = "http://anidb.net/api/animetitles.xml";
private readonly IHttpClient _httpClient;
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
/// <summary>
/// Creates a new instance of the <see cref="AniDbPrescanTask"/> class.
/// </summary>
/// <param name="config"></param>
/// <param name="httpClient"></param>
/// <param name="logger"></param>
public AniDbPrescanTask(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger)
{
_config = config;
_httpClient = httpClient;
_logger = logger;
}
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
if (!_config.Configuration.EnableInternetProviders)
{
progress.Report(100);
return;
}
var titlesFile = AniDbTitleMatcher.GetTitlesFile(_config.CommonApplicationPaths);
var titlesFileInfo = new FileInfo(titlesFile);
// download titles if we do not already have them, or have not updated for a week
if (!titlesFileInfo.Exists || (DateTime.UtcNow - titlesFileInfo.LastWriteTimeUtc).TotalDays > 7)
{
await DownloadTitles(titlesFile, cancellationToken).ConfigureAwait(false);
}
await AniDbTitleMatcher.DefaultInstance.Load().ConfigureAwait(false);
progress.Report(100);
}
/// <summary>
/// Downloads an xml file from AniDB which contains all of the titles for every anime, and their IDs,
/// and saves it to disk.
/// </summary>
/// <param name="titlesFile">The destination file name.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private async Task DownloadTitles(string titlesFile, CancellationToken cancellationToken)
{
_logger.Debug("Downloading new AniDB titles file.");
var request = new HttpRequestOptions
{
Url = TitlesUrl,
CancellationToken = cancellationToken,
EnableHttpCompression = true
};
using (var stream = await _httpClient.Get(request).ConfigureAwait(false))
using (var writer = File.Open(titlesFile, FileMode.Create, FileAccess.Write))
{
await stream.CopyToAsync(writer).ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Logging;
namespace MediaBrowser.Plugins.AniDB
{
/// <summary>
/// The <see cref="IAniDbTitleMatcher"/> interface defines a type which can match series titles to AniDB IDs.
/// </summary>
public interface IAniDbTitleMatcher
{
/// <summary>
/// Finds the AniDB for the series with the given title.
/// </summary>
/// <param name="title">The title of the series to search for.</param>
/// <returns>The AniDB ID of the series is found; else <c>null</c>.</returns>
Task<string> FindSeries(string title);
/// <summary>
/// Loads series titles from the series.xml file into memory.
/// </summary>
Task Load();
}
/// <summary>
/// The <see cref="AniDbTitleMatcher"/> class loads series titles from the series.xml file in the application data anidb folder,
/// and provides the means to search for a the AniDB of a series by series title.
/// </summary>
public class AniDbTitleMatcher : IAniDbTitleMatcher
{
//todo replace the singleton IAniDbTitleMatcher with an injected dependency if/when MediaBrowser allows plugins to register their own components
/// <summary>
/// Gets or sets the global <see cref="IAniDbTitleMatcher"/> instance.
/// </summary>
public static IAniDbTitleMatcher DefaultInstance { get; set; }
private readonly IApplicationPaths _paths;
private readonly ILogger _logger;
private Dictionary<string, string> _titles;
/// <summary>
/// Creates a new instance of the AniDbTitleMatcher class.
/// </summary>
/// <param name="paths">The application paths.</param>
/// <param name="logger">The logger.</param>
public AniDbTitleMatcher(IApplicationPaths paths, ILogger logger)
{
_paths = paths;
_logger = logger;
}
/// <summary>
/// Gets the path to the anidb data folder.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
/// <returns>The path to the anidb data folder.</returns>
public static string GetDataPath(IApplicationPaths applicationPaths)
{
return Path.Combine(applicationPaths.DataPath, "anidb");
}
/// <summary>
/// Gets the path to the titles.xml file.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
/// <returns>The path to the titles.xml file.</returns>
public static string GetTitlesFile(IApplicationPaths applicationPaths)
{
var data = GetDataPath(applicationPaths);
Directory.CreateDirectory(data);
return Path.Combine(data, "titles.xml");
}
public async Task<string> FindSeries(string title)
{
if (!IsLoaded)
{
await Load().ConfigureAwait(false);
}
string aid;
if (_titles.TryGetValue(title, out aid))
{
return aid;
}
return null;
}
public bool IsLoaded
{
get { return _titles != null; }
}
public async Task Load()
{
if (_titles == null)
{
_titles = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
else
{
_titles.Clear();
}
try
{
await ReadTitlesFile().ConfigureAwait(false);
}
catch (Exception e)
{
_logger.ErrorException("Failed to load AniDB titles", e);
}
}
private Task ReadTitlesFile()
{
return Task.Run(() =>
{
_logger.Debug("Loading AniDB titles");
var titlesFile = GetTitlesFile(_paths);
var settings = new XmlReaderSettings
{
CheckCharacters = false,
IgnoreProcessingInstructions = true,
IgnoreComments = true,
ValidationType = ValidationType.None
};
using (var stream = new StreamReader(titlesFile, Encoding.UTF8))
using (var reader = XmlReader.Create(stream, settings))
{
string aid = null;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "anime":
reader.MoveToAttribute("aid");
aid = reader.Value;
break;
case "title":
var title = reader.ReadElementContentAsString();
if (!string.IsNullOrEmpty(aid) && !string.IsNullOrEmpty(title))
{
_titles[title] = aid;
}
break;
}
}
}
}
});
}
}
}

View File

@ -0,0 +1,71 @@
<?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>{F68223A5-EE1E-444C-89F9-14092AAB987D}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MediaBrowser.Plugins.AniDB</RootNamespace>
<AssemblyName>MediaBrowser.Plugins.AniDB</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</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="MediaBrowser.Common">
<HintPath>..\packages\MediaBrowser.Common.3.0.232\lib\net45\MediaBrowser.Common.dll</HintPath>
</Reference>
<Reference Include="MediaBrowser.Controller">
<HintPath>..\packages\MediaBrowser.Server.Core.3.0.232\lib\net45\MediaBrowser.Controller.dll</HintPath>
</Reference>
<Reference Include="MediaBrowser.Model">
<HintPath>..\packages\MediaBrowser.Common.3.0.232\lib\net45\MediaBrowser.Model.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<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="AniDbPrescanTask.cs" />
<Compile Include="IAniDbTitleMatcher.cs" />
<Compile Include="Plugin.cs" />
<Compile Include="PluginConfiguration.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>xcopy "$(TargetPath)" "%25MediaBrowserData%25\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>

View File

@ -0,0 +1,21 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Plugins.AniDB
{
public class Plugin
: BasePlugin<PluginConfiguration>
{
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILogger logger) : base(applicationPaths, xmlSerializer)
{
AniDbTitleMatcher.DefaultInstance = new AniDbTitleMatcher(applicationPaths, logger);
}
public override string Name
{
get { return "AniDB"; }
}
}
}

View File

@ -0,0 +1,9 @@
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Plugins.AniDB
{
public class PluginConfiguration
: BasePluginConfiguration
{
}
}

View File

@ -0,0 +1,34 @@
using System.Reflection;
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.AniDB")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MediaBrowser.Plugins.AniDB")]
[assembly: AssemblyCopyright("Copyright © 2013")]
[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("1d0dddf7-1877-4473-8d7b-03f7dac1e559")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.*")]

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="MediaBrowser.Common" version="3.0.232" targetFramework="net45" />
<package id="MediaBrowser.Server.Core" version="3.0.232" targetFramework="net45" />
</packages>

View File

@ -2,3 +2,7 @@ MediaBrowser.Plugins.AniDB
==========================
An AniDB provider for Media Browser 3
## Compiling and Testing
You must have a %MediaBrowserData% environment variable pointing to the server data folder of the Media Browser server. The plugin will be copied into the plugins folder when the project is successfully built.