Merge branch 'develop'

This commit is contained in:
Mark McDowall 2014-03-31 08:34:56 -07:00
commit dcb226dde2
85 changed files with 2213 additions and 253 deletions

View File

@ -3,4 +3,5 @@
<dllmap os="osx" dll="MediaInfo.dll" target="libmediainfo.dylib"/> <dllmap os="osx" dll="MediaInfo.dll" target="libmediainfo.dylib"/>
<dllmap os="linux" dll="MediaInfo.dll" target="libmediainfo.so.0" /> <dllmap os="linux" dll="MediaInfo.dll" target="libmediainfo.so.0" />
<dllmap os="freebsd" dll="MediaInfo.dll" target="libmediainfo.so.0" /> <dllmap os="freebsd" dll="MediaInfo.dll" target="libmediainfo.so.0" />
<dllmap os="solaris" dll="MediaInfo.dll" target="libmediainfo.so.0.0.0" />
</configuration> </configuration>

View File

@ -95,6 +95,9 @@
</None> </None>
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\nuget.targets" /> <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -0,0 +1,66 @@
using Nancy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DDay.iCal;
using NzbDrone.Core.Tv;
using Nancy.Responses;
namespace NzbDrone.Api.Calendar
{
public class CalendarFeedModule : NzbDroneFeedModule
{
private readonly IEpisodeService _episodeService;
public CalendarFeedModule(IEpisodeService episodeService)
: base("calendar")
{
_episodeService = episodeService;
Get["/NzbDrone.ics"] = options => GetCalendarFeed();
}
private Response GetCalendarFeed()
{
var start = DateTime.Today.AddDays(-7);
var end = DateTime.Today.AddDays(28);
var queryStart = Request.Query.Start;
var queryEnd = Request.Query.End;
if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value);
if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value);
var episodes = _episodeService.EpisodesBetweenDates(start, end);
var icalCalendar = new iCalendar();
foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value))
{
var occurrence = icalCalendar.Create<Event>();
occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString();
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
occurrence.Start = new iCalDateTime(episode.AirDateUtc.Value);
occurrence.End = new iCalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime));
occurrence.Description = episode.Overview;
occurrence.Categories = new List<string>() { episode.Series.Network };
switch (episode.Series.SeriesType)
{
case SeriesTypes.Daily:
occurrence.Summary = string.Format("{0} - {1}", episode.Series.Title, episode.Title);
break;
default:
occurrence.Summary = string.Format("{0} - {1}x{2:00} - {3}", episode.Series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title);
break;
}
}
var serializer = new DDay.iCal.Serialization.iCalendar.SerializerFactory().Build(icalCalendar.GetType(), new DDay.iCal.Serialization.SerializationContext()) as DDay.iCal.Serialization.IStringSerializer;
var icalendar = serializer.SerializeToString(icalCalendar);
return new TextResponse(icalendar, "text/calendar");
}
}
}

View File

@ -26,7 +26,7 @@ namespace NzbDrone.Api.Frontend.Mappers
public override bool CanHandle(string resourceUrl) public override bool CanHandle(string resourceUrl)
{ {
return resourceUrl.StartsWith("/log/") && resourceUrl.EndsWith(".txt"); return resourceUrl.StartsWith("/logfile/") && resourceUrl.EndsWith(".txt");
} }
} }
} }

View File

@ -14,7 +14,7 @@ namespace NzbDrone.Api.Logs
public LogFileModule(IAppFolderInfo appFolderInfo, public LogFileModule(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider) IDiskProvider diskProvider)
: base("log/files") : base("log/file")
{ {
_appFolderInfo = appFolderInfo; _appFolderInfo = appFolderInfo;
_diskProvider = diskProvider; _diskProvider = diskProvider;

View File

@ -40,6 +40,9 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="DDay.iCal">
<HintPath>..\packages\DDay.iCal.1.0.2.575\lib\DDay.iCal.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AspNet.SignalR.Core, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <Reference Include="Microsoft.AspNet.SignalR.Core, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll</HintPath> <HintPath>..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll</HintPath>
@ -88,6 +91,7 @@
<Compile Include="Blacklist\BlacklistModule.cs" /> <Compile Include="Blacklist\BlacklistModule.cs" />
<Compile Include="Blacklist\BlacklistResource.cs" /> <Compile Include="Blacklist\BlacklistResource.cs" />
<Compile Include="Calendar\CalendarModule.cs" /> <Compile Include="Calendar\CalendarModule.cs" />
<Compile Include="Calendar\CalendarFeedModule.cs" />
<Compile Include="ClientSchema\SchemaDeserializer.cs" /> <Compile Include="ClientSchema\SchemaDeserializer.cs" />
<Compile Include="ClientSchema\FieldDefinitionAttribute.cs" /> <Compile Include="ClientSchema\FieldDefinitionAttribute.cs" />
<Compile Include="ClientSchema\Field.cs" /> <Compile Include="ClientSchema\Field.cs" />
@ -139,6 +143,7 @@
<Compile Include="Metadata\MetadataResource.cs" /> <Compile Include="Metadata\MetadataResource.cs" />
<Compile Include="Metadata\MetadataModule.cs" /> <Compile Include="Metadata\MetadataModule.cs" />
<Compile Include="Notifications\NotificationSchemaModule.cs" /> <Compile Include="Notifications\NotificationSchemaModule.cs" />
<Compile Include="NzbDroneFeedModule.cs" />
<Compile Include="ProviderResource.cs" /> <Compile Include="ProviderResource.cs" />
<Compile Include="ProviderModuleBase.cs" /> <Compile Include="ProviderModuleBase.cs" />
<Compile Include="Indexers\IndexerSchemaModule.cs" /> <Compile Include="Indexers\IndexerSchemaModule.cs" />
@ -199,7 +204,9 @@
<Compile Include="Validation\RuleBuilderExtensions.cs" /> <Compile Include="Validation\RuleBuilderExtensions.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config">
<SubType>Designer</SubType>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Marr.Data\Marr.Data.csproj"> <ProjectReference Include="..\Marr.Data\Marr.Data.csproj">

View File

@ -0,0 +1,12 @@
using Nancy;
namespace NzbDrone.Api
{
public abstract class NzbDroneFeedModule : NancyModule
{
protected NzbDroneFeedModule(string resource)
: base("/feed/" + resource.Trim('/'))
{
}
}
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="DDay.iCal" version="1.0.2.575" targetFramework="net40" />
<package id="FluentValidation" version="5.0.0.1" targetFramework="net40" /> <package id="FluentValidation" version="5.0.0.1" targetFramework="net40" />
<package id="Nancy" version="0.21.1" targetFramework="net40" /> <package id="Nancy" version="0.21.1" targetFramework="net40" />
<package id="Nancy.Authentication.Basic" version="0.21.1" targetFramework="net40" /> <package id="Nancy.Authentication.Basic" version="0.21.1" targetFramework="net40" />

View File

@ -95,6 +95,9 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\nuget.targets" /> <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
<PropertyGroup> <PropertyGroup>

View File

@ -82,6 +82,9 @@
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" /> <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -111,6 +111,9 @@
<ItemGroup> <ItemGroup>
<Folder Include="Properties\" /> <Folder Include="Properties\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\nuget.targets" /> <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -1,24 +0,0 @@
using System;
using System.Globalization;
namespace NzbDrone.Common
{
public static class DateTimeExtensions
{
public static DateTime GetFirstDayOfWeek(this DateTime dayInWeek)
{
var defaultCultureInfo = CultureInfo.CurrentCulture;
return GetFirstDayOfWeek(dayInWeek, defaultCultureInfo);
}
public static DateTime GetFirstDayOfWeek(this DateTime dayInWeek, CultureInfo cultureInfo)
{
DayOfWeek firstDay = cultureInfo.DateTimeFormat.FirstDayOfWeek;
DateTime firstDayInWeek = dayInWeek.Date;
while (firstDayInWeek.DayOfWeek != firstDay)
firstDayInWeek = firstDayInWeek.AddDays(-1);
return firstDayInWeek;
}
}
}

View File

@ -401,10 +401,15 @@ namespace NzbDrone.Common.Disk
{ {
if (File.Exists(path)) if (File.Exists(path))
{ {
var newAttributes = File.GetAttributes(path) & ~(FileAttributes.ReadOnly); var attributes = File.GetAttributes(path);
if (attributes.HasFlag(FileAttributes.ReadOnly))
{
var newAttributes = attributes & ~(FileAttributes.ReadOnly);
File.SetAttributes(path, newAttributes); File.SetAttributes(path, newAttributes);
} }
} }
}
public FileAttributes GetFileAttributes(string path) public FileAttributes GetFileAttributes(string path)
{ {

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Globalization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace NzbDrone.Common.EnvironmentInfo namespace NzbDrone.Common.EnvironmentInfo
@ -18,7 +19,7 @@ namespace NzbDrone.Common.EnvironmentInfo
IsLinux = IsMono && !IsOsx; IsLinux = IsMono && !IsOsx;
IsWindows = !IsMono; IsWindows = !IsMono;
FirstDayOfWeek = DateTime.Today.GetFirstDayOfWeek().DayOfWeek; FirstDayOfWeek = CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
if (!IsMono) if (!IsMono)
{ {

View File

@ -0,0 +1,21 @@
using System.IO;
namespace NzbDrone.Common.Extensions
{
public static class StreamExtensions
{
public static byte[] ToBytes(this Stream input)
{
var buffer = new byte[16 * 1024];
using (var ms = new MemoryStream())
{
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
}
}
}

View File

@ -66,7 +66,6 @@
<Compile Include="Composition\Container.cs" /> <Compile Include="Composition\Container.cs" />
<Compile Include="Composition\IContainer.cs" /> <Compile Include="Composition\IContainer.cs" />
<Compile Include="Composition\ContainerBuilderBase.cs" /> <Compile Include="Composition\ContainerBuilderBase.cs" />
<Compile Include="DateTimeExtensions.cs" />
<Compile Include="DictionaryExtensions.cs" /> <Compile Include="DictionaryExtensions.cs" />
<Compile Include="Disk\DiskProviderBase.cs" /> <Compile Include="Disk\DiskProviderBase.cs" />
<Compile Include="EnsureThat\Ensure.cs" /> <Compile Include="EnsureThat\Ensure.cs" />
@ -105,8 +104,10 @@
<Compile Include="PathEqualityComparer.cs" /> <Compile Include="PathEqualityComparer.cs" />
<Compile Include="Processes\INzbDroneProcessProvider.cs" /> <Compile Include="Processes\INzbDroneProcessProvider.cs" />
<Compile Include="Processes\ProcessOutput.cs" /> <Compile Include="Processes\ProcessOutput.cs" />
<Compile Include="RateGate.cs" />
<Compile Include="Serializer\IntConverter.cs" /> <Compile Include="Serializer\IntConverter.cs" />
<Compile Include="Services.cs" /> <Compile Include="Services.cs" />
<Compile Include="Extensions\StreamExtensions.cs" />
<Compile Include="TPL\LimitedConcurrencyLevelTaskScheduler.cs" /> <Compile Include="TPL\LimitedConcurrencyLevelTaskScheduler.cs" />
<Compile Include="Security\IgnoreCertErrorPolicy.cs" /> <Compile Include="Security\IgnoreCertErrorPolicy.cs" />
<Compile Include="StringExtensions.cs" /> <Compile Include="StringExtensions.cs" />

View File

@ -0,0 +1,197 @@
/*
* Code from: http://www.jackleitch.net/2010/10/better-rate-limiting-with-dot-net/
*/
using System;
using System.Collections.Concurrent;
using System.Threading;
namespace NzbDrone.Common
{
/// <summary>
/// Used to control the rate of some occurrence per unit of time.
/// </summary>
/// <remarks>
/// <para>
/// To control the rate of an action using a <see cref="RateGate"/>,
/// code should simply call <see cref="WaitToProceed()"/> prior to
/// performing the action. <see cref="WaitToProceed()"/> will block
/// the current thread until the action is allowed based on the rate
/// limit.
/// </para>
/// <para>
/// This class is thread safe. A single <see cref="RateGate"/> instance
/// may be used to control the rate of an occurrence across multiple
/// threads.
/// </para>
/// </remarks>
public class RateGate : IDisposable
{
// Semaphore used to count and limit the number of occurrences per
// unit time.
private readonly SemaphoreSlim _semaphore;
// Times (in millisecond ticks) at which the semaphore should be exited.
private readonly ConcurrentQueue<int> _exitTimes;
// Timer used to trigger exiting the semaphore.
private readonly Timer _exitTimer;
// Whether this instance is disposed.
private bool _isDisposed;
/// <summary>
/// Number of occurrences allowed per unit of time.
/// </summary>
public int Occurrences { get; private set; }
/// <summary>
/// The length of the time unit, in milliseconds.
/// </summary>
public int TimeUnitMilliseconds { get; private set; }
/// <summary>
/// Initializes a <see cref="RateGate"/> with a rate of <paramref name="occurrences"/>
/// per <paramref name="timeUnit"/>.
/// </summary>
/// <param name="occurrences">Number of occurrences allowed per unit of time.</param>
/// <param name="timeUnit">Length of the time unit.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// If <paramref name="occurrences"/> or <paramref name="timeUnit"/> is negative.
/// </exception>
public RateGate(int occurrences, TimeSpan timeUnit)
{
// Check the arguments.
if (occurrences <= 0)
throw new ArgumentOutOfRangeException("occurrences", "Number of occurrences must be a positive integer");
if (timeUnit != timeUnit.Duration())
throw new ArgumentOutOfRangeException("timeUnit", "Time unit must be a positive span of time");
if (timeUnit >= TimeSpan.FromMilliseconds(UInt32.MaxValue))
throw new ArgumentOutOfRangeException("timeUnit", "Time unit must be less than 2^32 milliseconds");
Occurrences = occurrences;
TimeUnitMilliseconds = (int)timeUnit.TotalMilliseconds;
// Create the semaphore, with the number of occurrences as the maximum count.
_semaphore = new SemaphoreSlim(Occurrences, Occurrences);
// Create a queue to hold the semaphore exit times.
_exitTimes = new ConcurrentQueue<int>();
// Create a timer to exit the semaphore. Use the time unit as the original
// interval length because that's the earliest we will need to exit the semaphore.
_exitTimer = new Timer(ExitTimerCallback, null, TimeUnitMilliseconds, -1);
}
// Callback for the exit timer that exits the semaphore based on exit times
// in the queue and then sets the timer for the nextexit time.
private void ExitTimerCallback(object state)
{
// While there are exit times that are passed due still in the queue,
// exit the semaphore and dequeue the exit time.
int exitTime;
while (_exitTimes.TryPeek(out exitTime)
&& unchecked(exitTime - Environment.TickCount) <= 0)
{
_semaphore.Release();
_exitTimes.TryDequeue(out exitTime);
}
// Try to get the next exit time from the queue and compute
// the time until the next check should take place. If the
// queue is empty, then no exit times will occur until at least
// one time unit has passed.
int timeUntilNextCheck;
if (_exitTimes.TryPeek(out exitTime))
timeUntilNextCheck = unchecked(exitTime - Environment.TickCount);
else
timeUntilNextCheck = TimeUnitMilliseconds;
// Set the timer.
_exitTimer.Change(timeUntilNextCheck, -1);
}
/// <summary>
/// Blocks the current thread until allowed to proceed or until the
/// specified timeout elapses.
/// </summary>
/// <param name="millisecondsTimeout">Number of milliseconds to wait, or -1 to wait indefinitely.</param>
/// <returns>true if the thread is allowed to proceed, or false if timed out</returns>
public bool WaitToProceed(int millisecondsTimeout)
{
// Check the arguments.
if (millisecondsTimeout < -1)
throw new ArgumentOutOfRangeException("millisecondsTimeout");
CheckDisposed();
// Block until we can enter the semaphore or until the timeout expires.
var entered = _semaphore.Wait(millisecondsTimeout);
// If we entered the semaphore, compute the corresponding exit time
// and add it to the queue.
if (entered)
{
var timeToExit = unchecked(Environment.TickCount + TimeUnitMilliseconds);
_exitTimes.Enqueue(timeToExit);
}
return entered;
}
/// <summary>
/// Blocks the current thread until allowed to proceed or until the
/// specified timeout elapses.
/// </summary>
/// <param name="timeout"></param>
/// <returns>true if the thread is allowed to proceed, or false if timed out</returns>
public bool WaitToProceed(TimeSpan timeout)
{
return WaitToProceed((int)timeout.TotalMilliseconds);
}
/// <summary>
/// Blocks the current thread indefinitely until allowed to proceed.
/// </summary>
public void WaitToProceed()
{
WaitToProceed(Timeout.Infinite);
}
// Throws an ObjectDisposedException if this object is disposed.
private void CheckDisposed()
{
if (_isDisposed)
throw new ObjectDisposedException("RateGate is already disposed");
}
/// <summary>
/// Releases unmanaged resources held by an instance of this class.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged resources held by an instance of this class.
/// </summary>
/// <param name="isDisposing">Whether this object is being disposed.</param>
protected virtual void Dispose(bool isDisposing)
{
if (!_isDisposed)
{
if (isDisposing)
{
// The semaphore and timer both implement IDisposable and
// therefore must be disposed.
_semaphore.Dispose();
_exitTimer.Dispose();
_isDisposed = true;
}
}
}
}
}

View File

@ -1,8 +1,10 @@
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.Nzbget; using NzbDrone.Core.Download.Clients.Nzbget;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@ -46,16 +48,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
[Test] [Test]
public void should_add_item_to_queue() public void should_add_item_to_queue()
{ {
var p = new object[] {"30.Rock.S01E01.Pilot.720p.hdtv.nzb", "TV", 50, false, "http://www.nzbdrone.com"};
Mocker.GetMock<INzbgetProxy>() Mocker.GetMock<INzbgetProxy>()
.Setup(s => s.AddNzb(It.IsAny<NzbgetSettings>(), p)) .Setup(s => s.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<NzbgetSettings>()))
.Returns(true); .Returns("id");
Subject.DownloadNzb(_remoteEpisode); Subject.DownloadNzb(_remoteEpisode);
Mocker.GetMock<INzbgetProxy>() Mocker.GetMock<INzbgetProxy>()
.Verify(v => v.AddNzb(It.IsAny<NzbgetSettings>(), It.IsAny<object []>()), Times.Once()); .Verify(v => v.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<NzbgetSettings>()), Times.Once());
} }
} }
} }

View File

@ -24,6 +24,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
_queue = Builder<NzbgetQueueItem>.CreateListOfSize(5) _queue = Builder<NzbgetQueueItem>.CreateListOfSize(5)
.All() .All()
.With(q => q.NzbName = "30.Rock.S01E01.Pilot.720p.hdtv.nzb") .With(q => q.NzbName = "30.Rock.S01E01.Pilot.720p.hdtv.nzb")
.With(q => q.Parameters = new List<NzbgetParameter>
{
new NzbgetParameter { Name = "drone", Value = "id" }
})
.Build() .Build()
.ToList(); .ToList();

View File

@ -0,0 +1,70 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupDuplicateMetadataFilesFixture : DbTest<CleanupDuplicateMetadataFiles, MetadataFile>
{
[Test]
public void should_not_delete_metadata_files_when_they_are_for_the_same_series_but_different_consumers()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.SeriesMetadata)
.With(m => m.SeriesId = 1)
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(files.Count);
}
[Test]
public void should_not_delete_metadata_files_for_different_series()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.SeriesMetadata)
.With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(files.Count);
}
[Test]
public void should_delete_metadata_files_when_they_are_for_the_same_series_and_consumer()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.SeriesMetadata)
.With(m => m.SeriesId = 1)
.With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(1);
}
[Test]
public void should_not_delete_metadata_files_when_there_is_only_one_for_that_series_and_consumer()
{
var file = Builder<MetadataFile>.CreateNew()
.BuildNew();
Db.Insert(file);
Subject.Clean();
AllStoredModels.Count.Should().Be(1);
}
}
}

View File

@ -1,12 +1,10 @@
using System; using System;
using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using Microsoft.Practices.ObjectBuilder2; using Microsoft.Practices.ObjectBuilder2;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Jobs; using NzbDrone.Core.Jobs;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers namespace NzbDrone.Core.Test.Housekeeping.Housekeepers

View File

@ -136,6 +136,7 @@
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFilesFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFilesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecsFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecsFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFilesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
<Compile Include="IndexerSearchTests\SearchDefinitionFixture.cs" /> <Compile Include="IndexerSearchTests\SearchDefinitionFixture.cs" />
<Compile Include="IndexerTests\BasicRssParserFixture.cs" /> <Compile Include="IndexerTests\BasicRssParserFixture.cs" />
@ -231,6 +232,7 @@
<Compile Include="TvTests\SeriesRepositoryTests\QualityProfileRepositoryFixture.cs" /> <Compile Include="TvTests\SeriesRepositoryTests\QualityProfileRepositoryFixture.cs" />
<Compile Include="TvTests\SeriesServiceTests\UpdateMultipleSeriesFixture.cs" /> <Compile Include="TvTests\SeriesServiceTests\UpdateMultipleSeriesFixture.cs" />
<Compile Include="TvTests\SeriesServiceTests\UpdateSeriesFixture.cs" /> <Compile Include="TvTests\SeriesServiceTests\UpdateSeriesFixture.cs" />
<Compile Include="TvTests\ShouldRefreshSeriesFixture.cs" />
<Compile Include="UpdateTests\UpdateServiceFixture.cs" /> <Compile Include="UpdateTests\UpdateServiceFixture.cs" />
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
<Compile Include="Qualities\QualityDefinitionServiceFixture.cs" /> <Compile Include="Qualities\QualityDefinitionServiceFixture.cs" />
@ -374,6 +376,9 @@
<ItemGroup> <ItemGroup>
<Folder Include="ProviderTests\UpdateProviderTests\" /> <Folder Include="ProviderTests\UpdateProviderTests\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup> <PropertyGroup>
<PreBuildEvent> <PreBuildEvent>

View File

@ -27,6 +27,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("86420f8ee425340d8894bf3bc636b66404b95f18")] [TestCase("86420f8ee425340d8894bf3bc636b66404b95f18")]
[TestCase("ce39afb7da6cf7c04eba3090f0a309f609883862")] [TestCase("ce39afb7da6cf7c04eba3090f0a309f609883862")]
[TestCase("THIS SHOULD NEVER PARSE")] [TestCase("THIS SHOULD NEVER PARSE")]
[TestCase("Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv")]
public void should_not_parse_crap(string title) public void should_not_parse_crap(string title)
{ {
Parser.Parser.ParseTitle(title).Should().BeNull(); Parser.Parser.ParseTitle(title).Should().BeNull();

View File

@ -31,6 +31,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase(@"C:\Test\Unsorted\The.Big.Bang.Theory.S01E01.720p.HDTV\tbbt101.avi", 1, 1)] [TestCase(@"C:\Test\Unsorted\The.Big.Bang.Theory.S01E01.720p.HDTV\tbbt101.avi", 1, 1)]
[TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E19.720p.BluRay.x264-SiNNERS-RP\ba27283b17c00d01193eacc02a8ba98eeb523a76.mkv", 2, 19)] [TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E19.720p.BluRay.x264-SiNNERS-RP\ba27283b17c00d01193eacc02a8ba98eeb523a76.mkv", 2, 19)]
[TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E18.720p.BluRay.x264-SiNNERS-RP\45a55debe3856da318cc35882ad07e43cd32fd15.mkv", 2, 18)] [TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E18.720p.BluRay.x264-SiNNERS-RP\45a55debe3856da318cc35882ad07e43cd32fd15.mkv", 2, 18)]
[TestCase(@"C:\Test\The.Blacklist.S01E16.720p.HDTV.X264-DIMENSION\XRmZciqkBopq4851Ddbipe\Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv", 1, 16)]
public void should_parse_from_path(string path, int season, int episode) public void should_parse_from_path(string path, int season, int episode)
{ {
var result = Parser.Parser.ParsePath(path); var result = Parser.Parser.ParsePath(path);

View File

@ -24,6 +24,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The Office Season4 WS PDTV XviD FUtV", "The Office", 4)] [TestCase("The Office Season4 WS PDTV XviD FUtV", "The Office", 4)]
[TestCase("Eureka S 01 720p WEB DL DD 5 1 h264 TjHD", "Eureka", 1)] [TestCase("Eureka S 01 720p WEB DL DD 5 1 h264 TjHD", "Eureka", 1)]
[TestCase("Doctor Who Confidential Season 3", "Doctor Who Confidential", 3)] [TestCase("Doctor Who Confidential Season 3", "Doctor Who Confidential", 3)]
[TestCase("Fleming.S01.720p.WEBDL.DD5.1.H.264-NTb", "Fleming", 1)]
public void should_parsefull_season_release(string postTitle, string title, int season) public void should_parsefull_season_release(string postTitle, string title, int season)
{ {
var result = Parser.Parser.ParseTitle(postTitle); var result = Parser.Parser.ParseTitle(postTitle);

View File

@ -4,10 +4,10 @@ using System.IO;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.RootFolderTests namespace NzbDrone.Core.Test.RootFolderTests
@ -28,7 +28,7 @@ namespace NzbDrone.Core.Test.RootFolderTests
.Returns(new List<RootFolder>()); .Returns(new List<RootFolder>());
} }
private void WithNoneExistingFolder() private void WithNonExistingFolder()
{ {
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(m => m.FolderExists(It.IsAny<string>())) .Setup(m => m.FolderExists(It.IsAny<string>()))
@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.RootFolderTests
[Test] [Test]
public void should_throw_if_folder_being_added_doesnt_exist() public void should_throw_if_folder_being_added_doesnt_exist()
{ {
WithNoneExistingFolder(); WithNonExistingFolder();
Assert.Throws<DirectoryNotFoundException>(() => Subject.Add(new RootFolder { Path = "C:\\TEST".AsOsAgnostic() })); Assert.Throws<DirectoryNotFoundException>(() => Subject.Add(new RootFolder { Path = "C:\\TEST".AsOsAgnostic() }));
} }
@ -62,9 +62,9 @@ namespace NzbDrone.Core.Test.RootFolderTests
} }
[Test] [Test]
public void None_existing_folder_returns_empty_list() public void should_return_empty_list_when_folder_doesnt_exist()
{ {
WithNoneExistingFolder(); WithNonExistingFolder();
Mocker.GetMock<IRootFolderRepository>().Setup(c => c.All()).Returns(new List<RootFolder>()); Mocker.GetMock<IRootFolderRepository>().Setup(c => c.All()).Returns(new List<RootFolder>());
@ -100,5 +100,26 @@ namespace NzbDrone.Core.Test.RootFolderTests
Assert.Throws<InvalidOperationException>(() => Subject.Add(new RootFolder { Path = @"C:\TV".AsOsAgnostic() })); Assert.Throws<InvalidOperationException>(() => Subject.Add(new RootFolder { Path = @"C:\TV".AsOsAgnostic() }));
} }
[Test]
public void should_not_include_system_files_and_folders()
{
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.GetDirectories(It.IsAny<String>()))
.Returns(new string[]
{
@"C:\30 Rock".AsOsAgnostic(),
@"C:\$Recycle.Bin".AsOsAgnostic(),
@"C:\.AppleDouble".AsOsAgnostic(),
@"C:\Test\.AppleDouble".AsOsAgnostic()
});
Mocker.GetMock<ISeriesService>()
.Setup(s => s.GetAllSeries())
.Returns(new List<Series>());
Subject.GetUnmappedFolders(@"C:\")
.Should().OnlyContain(u => u.Path == @"C:\30 Rock".AsOsAgnostic());
}
} }
} }

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.TvTests
{
[TestFixture]
public class ShouldRefreshSeriesFixture : TestBase<ShouldRefreshSeries>
{
private Series _series;
[SetUp]
public void Setup()
{
_series = Builder<Series>.CreateNew()
.Build();
}
private void GivenSeriesIsEnded()
{
_series.Status = SeriesStatusType.Ended;
}
private void GivenSeriesLastRefreshedRecently()
{
_series.LastInfoSync = DateTime.UtcNow.AddDays(-1);
}
[Test]
public void should_return_true_if_series_is_continuing()
{
_series.Status = SeriesStatusType.Continuing;
Subject.ShouldRefresh(_series).Should().BeTrue();
}
[Test]
public void should_return_true_if_series_last_refreshed_more_than_30_days_ago()
{
GivenSeriesIsEnded();
_series.LastInfoSync = DateTime.UtcNow.AddDays(-100);
Subject.ShouldRefresh(_series).Should().BeTrue();
}
[Test]
public void should_should_return_true_if_episode_aired_in_last_30_days()
{
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.GetEpisodeBySeries(_series.Id))
.Returns(Builder<Episode>.CreateListOfSize(2)
.TheFirst(1)
.With(e => e.AirDateUtc = DateTime.Today.AddDays(-7))
.TheLast(1)
.With(e => e.AirDateUtc = DateTime.Today.AddDays(-100))
.Build()
.ToList());
Subject.ShouldRefresh(_series).Should().BeTrue();
}
[Test]
public void should_should_return_false_when_recently_refreshed_ended_show_has_not_aired_for_30_days()
{
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.GetEpisodeBySeries(_series.Id))
.Returns(Builder<Episode>.CreateListOfSize(2)
.All()
.With(e => e.AirDateUtc = DateTime.Today.AddDays(-100))
.Build()
.ToList());
Subject.ShouldRefresh(_series).Should().BeTrue();
}
}
}

View File

@ -15,7 +15,6 @@ namespace NzbDrone.Core.Datastore
IDatabase Create(MigrationType migrationType = MigrationType.Main); IDatabase Create(MigrationType migrationType = MigrationType.Main);
} }
public class DbFactory : IDbFactory public class DbFactory : IDbFactory
{ {
private readonly IMigrationController _migrationController; private readonly IMigrationController _migrationController;
@ -79,8 +78,11 @@ namespace NzbDrone.Core.Datastore
return dataMapper; return dataMapper;
}); });
db.Vacuum();
if (migrationType == MigrationType.Main)
{
db.Vacuum();
}
return db; return db;
} }

View File

@ -97,6 +97,11 @@ namespace NzbDrone.Core.DecisionEngine
if (decision != null) if (decision != null)
{ {
if (decision.Rejections.Any())
{
_logger.Debug("Release rejected for the following reasons: {0}", String.Join(", ", decision.Rejections));
}
yield return decision; yield return decision;
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.Nzbget namespace NzbDrone.Core.Download.Clients.Nzbget
{ {
@ -6,10 +7,13 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{ {
private string _nzbName; private string _nzbName;
public Int32 NzbId { get; set; } public Int32 NzbId { get; set; }
public Int32 FirstId { get; set; }
public Int32 LastId { get; set; }
public string NzbName { get; set; } public string NzbName { get; set; }
public String Category { get; set; } public String Category { get; set; }
public Int32 FileSizeMb { get; set; } public Int32 FileSizeMb { get; set; }
public Int32 RemainingSizeMb { get; set; } public Int32 RemainingSizeMb { get; set; }
public Int32 PausedSizeMb { get; set; } public Int32 PausedSizeMb { get; set; }
public List<NzbgetParameter> Parameters { get; set; }
} }
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@ -13,14 +14,17 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{ {
private readonly INzbgetProxy _proxy; private readonly INzbgetProxy _proxy;
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
private readonly IHttpProvider _httpProvider;
private readonly Logger _logger; private readonly Logger _logger;
public Nzbget(INzbgetProxy proxy, public Nzbget(INzbgetProxy proxy,
IParsingService parsingService, IParsingService parsingService,
IHttpProvider httpProvider,
Logger logger) Logger logger)
{ {
_proxy = proxy; _proxy = proxy;
_parsingService = parsingService; _parsingService = parsingService;
_httpProvider = httpProvider;
_logger = logger; _logger = logger;
} }
@ -29,16 +33,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var url = remoteEpisode.Release.DownloadUrl; var url = remoteEpisode.Release.DownloadUrl;
var title = remoteEpisode.Release.Title + ".nzb"; var title = remoteEpisode.Release.Title + ".nzb";
string cat = Settings.TvCategory; string category = Settings.TvCategory;
int priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority; int priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
_logger.Info("Adding report [{0}] to the queue.", title); _logger.Info("Adding report [{0}] to the queue.", title);
var success = _proxy.AddNzb(Settings, title, cat, priority, false, url); using (var nzb = _httpProvider.DownloadStream(url))
{
_logger.Info("Adding report [{0}] to the queue.", title);
var response = _proxy.DownloadNzb(nzb, title, category, priority, Settings);
_logger.Debug("Queue Response: [{0}]", success); return response;
}
return null;
} }
public override IEnumerable<QueueItem> GetQueue() public override IEnumerable<QueueItem> GetQueue()
@ -57,14 +63,16 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var queueItems = new List<QueueItem>(); var queueItems = new List<QueueItem>();
foreach (var nzbGetQueueItem in queue) foreach (var item in queue)
{ {
var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone");
var queueItem = new QueueItem(); var queueItem = new QueueItem();
queueItem.Id = nzbGetQueueItem.NzbId.ToString(); queueItem.Id = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString();
queueItem.Title = nzbGetQueueItem.NzbName; queueItem.Title = item.NzbName;
queueItem.Size = nzbGetQueueItem.FileSizeMb; queueItem.Size = item.FileSizeMb;
queueItem.Sizeleft = nzbGetQueueItem.RemainingSizeMb; queueItem.Sizeleft = item.RemainingSizeMb;
queueItem.Status = nzbGetQueueItem.FileSizeMb == nzbGetQueueItem.PausedSizeMb ? "paused" : "queued"; queueItem.Status = item.FileSizeMb == item.PausedSizeMb ? "paused" : "queued";
var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title); var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title);
if (parsedEpisodeInfo == null) continue; if (parsedEpisodeInfo == null) continue;
@ -81,7 +89,43 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
public override IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10) public override IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10)
{ {
return new HistoryItem[0]; List<NzbgetHistoryItem> history;
try
{
history = _proxy.GetHistory(Settings);
}
catch (DownloadClientException ex)
{
_logger.ErrorException(ex.Message, ex);
return Enumerable.Empty<HistoryItem>();
}
var historyItems = new List<HistoryItem>();
var successStatues = new[] {"SUCCESS", "NONE"};
foreach (var item in history)
{
var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone");
var status = successStatues.Contains(item.ParStatus) &&
successStatues.Contains(item.ScriptStatus)
? HistoryStatus.Completed
: HistoryStatus.Failed;
var historyItem = new HistoryItem();
historyItem.Id = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString();
historyItem.Title = item.Name;
historyItem.Size = item.FileSizeMb.ToString(); //Why is this a string?
historyItem.DownloadTime = 0;
historyItem.Storage = item.DestDir;
historyItem.Category = item.Category;
historyItem.Message = String.Format("PAR Status: {0} - Script Status: {1}", item.ParStatus, item.ScriptStatus);
historyItem.Status = status;
historyItems.Add(historyItem);
}
return historyItems;
} }
public override void RemoveFromQueue(string id) public override void RemoveFromQueue(string id)
@ -91,7 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
public override void RemoveFromHistory(string id) public override void RemoveFromHistory(string id)
{ {
throw new NotImplementedException(); _proxy.RemoveFromHistory(id, Settings);
} }
public override void Test() public override void Test()

View File

@ -2,7 +2,7 @@
namespace NzbDrone.Core.Download.Clients.Nzbget namespace NzbDrone.Core.Download.Clients.Nzbget
{ {
public class EnqueueResponse public class NzbgetBooleanResponse
{ {
public String Version { get; set; } public String Version { get; set; }
public Boolean Result { get; set; } public Boolean Result { get; set; }

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.Nzbget
{
public class NzbgetHistoryItem
{
private string _nzbName;
public Int32 Id { get; set; }
public String Name { get; set; }
public String Category { get; set; }
public Int32 FileSizeMb { get; set; }
public String ParStatus { get; set; }
public String ScriptStatus { get; set; }
public String DestDir { get; set; }
public List<NzbgetParameter> Parameters { get; set; }
}
}

View File

@ -4,11 +4,11 @@ using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.Nzbget namespace NzbDrone.Core.Download.Clients.Nzbget
{ {
public class NzbgetQueue public class NzbgetListResponse<T>
{ {
public String Version { get; set; } public String Version { get; set; }
[JsonProperty(PropertyName = "result")] [JsonProperty(PropertyName = "result")]
public List<NzbgetQueueItem> QueueItems { get; set; } public List<T> QueueItems { get; set; }
} }
} }

View File

@ -0,0 +1,9 @@
using System;
namespace NzbDrone.Core.Download.Clients.Nzbget
{
public class NzbgetParameter
{
public String Name { get; set; }
public object Value { get; set; }
}
}

View File

@ -1,6 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Rest; using NzbDrone.Core.Rest;
using RestSharp; using RestSharp;
@ -9,9 +12,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{ {
public interface INzbgetProxy public interface INzbgetProxy
{ {
bool AddNzb(NzbgetSettings settings, params object[] parameters); string DownloadNzb(Stream nzb, string title, string category, int priority, NzbgetSettings settings);
List<NzbgetQueueItem> GetQueue(NzbgetSettings settings); List<NzbgetQueueItem> GetQueue(NzbgetSettings settings);
List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings);
VersionResponse GetVersion(NzbgetSettings settings); VersionResponse GetVersion(NzbgetSettings settings);
void RemoveFromHistory(string id, NzbgetSettings settings);
} }
public class NzbgetProxy : INzbgetProxy public class NzbgetProxy : INzbgetProxy
@ -23,18 +28,50 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
_logger = logger; _logger = logger;
} }
public bool AddNzb(NzbgetSettings settings, params object[] parameters) public string DownloadNzb(Stream nzb, string title, string category, int priority, NzbgetSettings settings)
{ {
var request = BuildRequest(new JsonRequest("appendurl", parameters)); var parameters = new object[] { title, category, priority, false, Convert.ToBase64String(nzb.ToBytes()) };
var request = BuildRequest(new JsonRequest("append", parameters));
return Json.Deserialize<EnqueueResponse>(ProcessRequest(request, settings)).Result; var response = Json.Deserialize<NzbgetBooleanResponse>(ProcessRequest(request, settings));
_logger.Debug("Queue Response: [{0}]", response.Result);
if (!response.Result)
{
return null;
}
var queue = GetQueue(settings);
var item = queue.FirstOrDefault(q => q.NzbName == title.Substring(0, title.Length - 4));
if (item == null)
{
return null;
}
var droneId = Guid.NewGuid().ToString().Replace("-", "");
var editResult = EditQueue("GroupSetParameter", 0, "drone=" + droneId, item.LastId, settings);
if (editResult)
{
_logger.Debug("Nzbget download drone parameter set to: {0}", droneId);
}
return droneId;
} }
public List<NzbgetQueueItem> GetQueue(NzbgetSettings settings) public List<NzbgetQueueItem> GetQueue(NzbgetSettings settings)
{ {
var request = BuildRequest(new JsonRequest("listgroups")); var request = BuildRequest(new JsonRequest("listgroups"));
return Json.Deserialize<NzbgetQueue>(ProcessRequest(request, settings)).QueueItems; return Json.Deserialize<NzbgetListResponse<NzbgetQueueItem>>(ProcessRequest(request, settings)).QueueItems;
}
public List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings)
{
var request = BuildRequest(new JsonRequest("history"));
return Json.Deserialize<NzbgetListResponse<NzbgetHistoryItem>>(ProcessRequest(request, settings)).QueueItems;
} }
public VersionResponse GetVersion(NzbgetSettings settings) public VersionResponse GetVersion(NzbgetSettings settings)
@ -44,6 +81,32 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
return Json.Deserialize<VersionResponse>(ProcessRequest(request, settings)); return Json.Deserialize<VersionResponse>(ProcessRequest(request, settings));
} }
public void RemoveFromHistory(string id, NzbgetSettings settings)
{
var history = GetHistory(settings);
var item = history.SingleOrDefault(h => h.Parameters.SingleOrDefault(p => p.Name == "drone") != null);
if (item == null)
{
_logger.Warn("Unable to remove item from nzbget's history, Unknown ID: {0}", id);
return;
}
if (!EditQueue("HistoryDelete", 0, "", item.Id, settings))
{
_logger.Warn("Failed to remove item from nzbget history, {0} [{1}]", item.Name, item.Id);
}
}
private bool EditQueue(string command, int offset, string editText, int id, NzbgetSettings settings)
{
var parameters = new object[] { command, offset, editText, id };
var request = BuildRequest(new JsonRequest("editqueue", parameters));
var response = Json.Deserialize<NzbgetBooleanResponse>(ProcessRequest(request, settings));
return response.Result;
}
private string ProcessRequest(IRestRequest restRequest, NzbgetSettings settings) private string ProcessRequest(IRestRequest restRequest, NzbgetSettings settings)
{ {
var client = BuildClient(settings); var client = BuildClient(settings);

View File

@ -3,6 +3,7 @@ using System.IO;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; using NzbDrone.Core.Download.Clients.Sabnzbd.Responses;
using NzbDrone.Core.Instrumentation.Extensions; using NzbDrone.Core.Instrumentation.Extensions;
@ -35,7 +36,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
var request = new RestRequest(Method.POST); var request = new RestRequest(Method.POST);
var action = String.Format("mode=addfile&cat={0}&priority={1}", category, priority); var action = String.Format("mode=addfile&cat={0}&priority={1}", category, priority);
request.AddFile("name", ReadFully(nzb), title, "application/x-nzb"); request.AddFile("name", nzb.ToBytes(), title, "application/x-nzb");
SabnzbdAddResponse response; SabnzbdAddResponse response;
@ -161,20 +162,5 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
if (result.Failed) if (result.Failed)
throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error); throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error);
} }
//TODO: Find a better home for this
private byte[] ReadFully(Stream input)
{
byte[] buffer = new byte[16 * 1024];
using (MemoryStream ms = new MemoryStream())
{
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
}
} }
} }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NLog;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;

View File

@ -0,0 +1,37 @@
using NLog;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class CleanupDuplicateMetadataFiles : IHousekeepingTask
{
private readonly IDatabase _database;
private readonly Logger _logger;
public CleanupDuplicateMetadataFiles(IDatabase database, Logger logger)
{
_database = database;
_logger = logger;
}
public void Clean()
{
_logger.Debug("Running cleanup of duplicate metadata files");
DeleteDuplicateSeriesMetadata();
}
private void DeleteDuplicateSeriesMetadata()
{
var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type = 1
GROUP BY SeriesId, Consumer
HAVING COUNT(SeriesId) > 1
)");
}
}
}

View File

@ -3,7 +3,7 @@ using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.IndexerSearch namespace NzbDrone.Core.IndexerSearch
{ {
public class EpisodeSearchCommand : Command public class MissingEpisodeSearchCommand : Command
{ {
public List<int> EpisodeIds { get; set; } public List<int> EpisodeIds { get; set; }

View File

@ -1,22 +1,30 @@
using NLog; using System;
using System.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Instrumentation.Extensions; using NzbDrone.Core.Instrumentation.Extensions;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.IndexerSearch namespace NzbDrone.Core.IndexerSearch
{ {
public class EpisodeSearchService : IExecute<EpisodeSearchCommand> public class MissingEpisodeSearchService : IExecute<EpisodeSearchCommand>, IExecute<MissingEpisodeSearchCommand>
{ {
private readonly ISearchForNzb _nzbSearchService; private readonly ISearchForNzb _nzbSearchService;
private readonly IDownloadApprovedReports _downloadApprovedReports; private readonly IDownloadApprovedReports _downloadApprovedReports;
private readonly IEpisodeService _episodeService;
private readonly Logger _logger; private readonly Logger _logger;
public EpisodeSearchService(ISearchForNzb nzbSearchService, public MissingEpisodeSearchService(ISearchForNzb nzbSearchService,
IDownloadApprovedReports downloadApprovedReports, IDownloadApprovedReports downloadApprovedReports,
IEpisodeService episodeService,
Logger logger) Logger logger)
{ {
_nzbSearchService = nzbSearchService; _nzbSearchService = nzbSearchService;
_downloadApprovedReports = downloadApprovedReports; _downloadApprovedReports = downloadApprovedReports;
_episodeService = episodeService;
_logger = logger; _logger = logger;
} }
@ -30,5 +38,39 @@ namespace NzbDrone.Core.IndexerSearch
_logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count); _logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count);
} }
} }
public void Execute(MissingEpisodeSearchCommand message)
{
//TODO: Look at ways to make this more efficient (grouping by series/season)
var episodes =
_episodeService.EpisodesWithoutFiles(new PagingSpec<Episode>
{
Page = 1,
PageSize = 100000,
SortDirection = SortDirection.Ascending,
SortKey = "Id",
FilterExpression = v => v.Monitored && v.Series.Monitored
}).Records.ToList();
_logger.ProgressInfo("Performing missing search for {0} episodes", episodes.Count);
var downloadedCount = 0;
//Limit requests to indexers at 100 per minute
using (var rateGate = new RateGate(100, TimeSpan.FromSeconds(60)))
{
foreach (var episode in episodes)
{
rateGate.WaitToProceed();
var decisions = _nzbSearchService.EpisodeSearch(episode);
var downloaded = _downloadApprovedReports.DownloadApproved(decisions);
downloadedCount += downloaded.Count;
_logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count);
}
}
_logger.ProgressInfo("Completed missing search for {0} episodes. {1} reports downloaded.", episodes.Count, downloadedCount);
}
} }
} }

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.IndexerSearch
{
public class EpisodeSearchCommand : Command
{
public List<int> EpisodeIds { get; set; }
public override bool SendUpdatesToClient
{
get
{
return true;
}
}
}
}

View File

@ -19,6 +19,7 @@ namespace NzbDrone.Core.IndexerSearch
public interface ISearchForNzb public interface ISearchForNzb
{ {
List<DownloadDecision> EpisodeSearch(int episodeId); List<DownloadDecision> EpisodeSearch(int episodeId);
List<DownloadDecision> EpisodeSearch(Episode episode);
List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber); List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber);
} }
@ -52,6 +53,12 @@ namespace NzbDrone.Core.IndexerSearch
public List<DownloadDecision> EpisodeSearch(int episodeId) public List<DownloadDecision> EpisodeSearch(int episodeId)
{ {
var episode = _episodeService.GetEpisode(episodeId); var episode = _episodeService.GetEpisode(episodeId);
return EpisodeSearch(episode);
}
public List<DownloadDecision> EpisodeSearch(Episode episode)
{
var series = _seriesService.GetSeries(episode.SeriesId); var series = _seriesService.GetSeries(episode.SeriesId);
if (series.SeriesType == SeriesTypes.Daily) if (series.SeriesType == SeriesTypes.Daily)
@ -67,7 +74,7 @@ namespace NzbDrone.Core.IndexerSearch
if (episode.SeasonNumber == 0) if (episode.SeasonNumber == 0)
{ {
// search for special episodes in season 0 // search for special episodes in season 0
return SearchSpecial(series, new List<Episode>{episode}); return SearchSpecial(series, new List<Episode> { episode });
} }
return SearchSingle(series, episode); return SearchSingle(series, episode);

View File

@ -0,0 +1,17 @@
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Indexers.Exceptions
{
public class RequestLimitReachedException : NzbDroneException
{
public RequestLimitReachedException(string message, params object[] args)
: base(message, args)
{
}
public RequestLimitReachedException(string message)
: base(message)
{
}
}
}

View File

@ -24,6 +24,11 @@ namespace NzbDrone.Core.Indexers.Newznab
throw new ApiKeyException("Indexer requires an API key"); throw new ApiKeyException("Indexer requires an API key");
} }
if (errorMessage == "Request limit reached")
{
throw new RequestLimitReachedException("API limit reached");
}
throw new NewznabException("Newznab error detected: {0}", errorMessage); throw new NewznabException("Newznab error detected: {0}", errorMessage);
} }
} }

View File

@ -46,7 +46,11 @@ namespace NzbDrone.Core.Indexers
_logger.Warn("Indexer returned result for Newznab RSS URL, API Key appears to be invalid"); _logger.Warn("Indexer returned result for Newznab RSS URL, API Key appears to be invalid");
var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key"); var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key");
throw new ValidationException(new List<ValidationFailure> { apiKeyFailure }.ToArray()); throw new ValidationException(new List<ValidationFailure> {apiKeyFailure}.ToArray());
}
catch (RequestLimitReachedException)
{
_logger.Warn("Request limit reached");
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -48,7 +48,6 @@ namespace NzbDrone.Core.Jobs
{ {
var defaultTasks = new[] var defaultTasks = new[]
{ {
new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName},
new ScheduledTask{ Interval = 5, TypeName = typeof(CheckHealthCommand).FullName}, new ScheduledTask{ Interval = 5, TypeName = typeof(CheckHealthCommand).FullName},
@ -73,7 +72,7 @@ namespace NzbDrone.Core.Jobs
var currentTasks = _scheduledTaskRepository.All().ToList(); var currentTasks = _scheduledTaskRepository.All().ToList();
_logger.Debug("Initializing jobs. Available: {0} Existing:{1}", defaultTasks.Count(), currentTasks.Count()); _logger.Debug("Initializing jobs. Available: {0} Existing: {1}", defaultTasks.Count(), currentTasks.Count());
foreach (var job in currentTasks) foreach (var job in currentTasks)
{ {

View File

@ -4,6 +4,14 @@ namespace NzbDrone.Core.MediaFiles.Commands
{ {
public class DownloadedEpisodesScanCommand : Command public class DownloadedEpisodesScanCommand : Command
{ {
public override bool SendUpdatesToClient
{
get
{
return SendUpdates;
}
}
public bool SendUpdates { get; set; }
} }
} }

View File

@ -13,5 +13,14 @@ namespace NzbDrone.Core.MediaFiles.Commands
return true; return true;
} }
} }
public RescanSeriesCommand()
{
}
public RescanSeriesCommand(int seriesId)
{
SeriesId = seriesId;
}
} }
} }

View File

@ -1,4 +1,5 @@
using System.IO; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@ -16,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles
{ {
public interface IDiskScanService public interface IDiskScanService
{ {
void Scan(Series series);
string[] GetVideoFiles(string path, bool allDirectories = true); string[] GetVideoFiles(string path, bool allDirectories = true);
} }
@ -52,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles
_logger = logger; _logger = logger;
} }
private void Scan(Series series) public void Scan(Series series)
{ {
_logger.ProgressInfo("Scanning disk for {0}", series.Title); _logger.ProgressInfo("Scanning disk for {0}", series.Title);
_commandExecutor.PublishCommand(new CleanMediaFileDb(series.Id)); _commandExecutor.PublishCommand(new CleanMediaFileDb(series.Id));
@ -73,9 +75,16 @@ namespace NzbDrone.Core.MediaFiles
return; return;
} }
var videoFilesStopwatch = Stopwatch.StartNew();
var mediaFileList = GetVideoFiles(series.Path).ToList(); var mediaFileList = GetVideoFiles(series.Path).ToList();
videoFilesStopwatch.Stop();
_logger.Trace("Finished getting episode files for: {0} [{1}]", series, videoFilesStopwatch.Elapsed);
var decisionsStopwatch = Stopwatch.StartNew();
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series, false); var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series, false);
decisionsStopwatch.Stop();
_logger.Trace("Import decisions complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed);
_importApprovedEpisodes.Import(decisions); _importApprovedEpisodes.Import(decisions);
_logger.Info("Completed scanning disk for {0}", series.Title); _logger.Info("Completed scanning disk for {0}", series.Title);

View File

@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.Fake
{
public class FakeMetadata : MetadataBase<FakeMetadataSettings>
{
public FakeMetadata(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger)
: base(diskProvider, httpProvider, logger)
{
}
public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
{
throw new NotImplementedException();
}
public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
{
throw new NotImplementedException();
}
public override void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
{
throw new NotImplementedException();
}
public override MetadataFile FindMetadataFile(Series series, string path)
{
return null;
}
}
}

View File

@ -1,41 +0,0 @@
using System;
using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Metadata.Consumers.Fake
{
public class FakeMetadataSettingsValidator : AbstractValidator<FakeMetadataSettings>
{
public FakeMetadataSettingsValidator()
{
}
}
public class FakeMetadataSettings : IProviderConfig
{
private static readonly FakeMetadataSettingsValidator Validator = new FakeMetadataSettingsValidator();
public FakeMetadataSettings()
{
FakeSetting = true;
}
[FieldDefinition(0, Label = "Fake Setting", Type = FieldType.Checkbox)]
public Boolean FakeSetting { get; set; }
public bool IsValid
{
get
{
return true;
}
}
public ValidationResult Validate()
{
return Validator.Validate(this);
}
}
}

View File

@ -0,0 +1,468 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.Roksbox
{
public class RoksboxMetadata : MetadataBase<RoksboxMetadataSettings>
{
private readonly IEventAggregator _eventAggregator;
private readonly IMapCoversToLocal _mediaCoverService;
private readonly IMediaFileService _mediaFileService;
private readonly IMetadataFileService _metadataFileService;
private readonly IDiskProvider _diskProvider;
private readonly IHttpProvider _httpProvider;
private readonly IEpisodeService _episodeService;
private readonly Logger _logger;
public RoksboxMetadata(IEventAggregator eventAggregator,
IMapCoversToLocal mediaCoverService,
IMediaFileService mediaFileService,
IMetadataFileService metadataFileService,
IDiskProvider diskProvider,
IHttpProvider httpProvider,
IEpisodeService episodeService,
Logger logger)
: base(diskProvider, httpProvider, logger)
{
_eventAggregator = eventAggregator;
_mediaCoverService = mediaCoverService;
_mediaFileService = mediaFileService;
_metadataFileService = metadataFileService;
_diskProvider = diskProvider;
_httpProvider = httpProvider;
_episodeService = episodeService;
_logger = logger;
}
private static List<string> ValidCertification = new List<string> { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" };
private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<season>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
{
var metadataFiles = new List<MetadataFile>();
if (!_diskProvider.FolderExists(series.Path))
{
_logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path);
return;
}
if (Settings.SeriesImages)
{
var metadata = WriteSeriesImages(series, existingMetadataFiles);
if (metadata != null)
{
metadataFiles.Add(metadata);
}
}
if (Settings.SeasonImages)
{
var metadata = WriteSeasonImages(series, existingMetadataFiles);
if (metadata != null)
{
metadataFiles.AddRange(metadata);
}
}
foreach (var episodeFile in episodeFiles)
{
if (Settings.EpisodeMetadata)
{
var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles);
if (metadata != null)
{
metadataFiles.Add(metadata);
}
}
}
foreach (var episodeFile in episodeFiles)
{
if (Settings.EpisodeImages)
{
var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles);
if (metadataFile != null)
{
metadataFiles.Add(metadataFile);
}
}
}
metadataFiles.RemoveAll(c => c == null);
_eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
}
public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
{
var metadataFiles = new List<MetadataFile>();
if (Settings.EpisodeMetadata)
{
metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List<MetadataFile>()));
}
if (Settings.EpisodeImages)
{
var metadataFile = WriteEpisodeImages(series, episodeFile, new List<MetadataFile>());
if (metadataFile != null)
{
metadataFiles.Add(metadataFile);
}
}
_eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
}
public override void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
{
var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
var updatedMetadataFiles = new List<MetadataFile>();
foreach (var episodeFile in episodeFiles)
{
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
foreach (var metadataFile in metadataFiles)
{
string newFilename;
if (metadataFile.Type == MetadataType.EpisodeImage)
{
newFilename = GetEpisodeImageFilename(episodeFile.Path);
}
else if (metadataFile.Type == MetadataType.EpisodeMetadata)
{
newFilename = GetEpisodeMetadataFilename(episodeFile.Path);
}
else
{
_logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath);
continue;
}
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath);
if (!newFilename.PathEquals(existingFilename))
{
_diskProvider.MoveFile(existingFilename, newFilename);
metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename);
updatedMetadataFiles.Add(metadataFile);
}
}
}
_eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles));
}
public override MetadataFile FindMetadataFile(Series series, string path)
{
var filename = Path.GetFileName(path);
if (filename == null) return null;
var parentdir = Directory.GetParent(path);
var metadata = new MetadataFile
{
SeriesId = series.Id,
Consumer = GetType().Name,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
};
//Series and season images are both named folder.jpg, only season ones sit in season folders
if (String.Compare(filename, parentdir.Name, true) == 0)
{
var seasonMatch = SeasonImagesRegex.Match(parentdir.Name);
if (seasonMatch.Success)
{
metadata.Type = MetadataType.SeasonImage;
var seasonNumber = seasonMatch.Groups["season"].Value;
if (seasonNumber.Contains("specials"))
{
metadata.SeasonNumber = 0;
}
else
{
metadata.SeasonNumber = Convert.ToInt32(seasonNumber);
}
return metadata;
}
else
{
metadata.Type = MetadataType.SeriesImage;
return metadata;
}
}
var parseResult = Parser.Parser.ParseTitle(filename);
if (parseResult != null &&
!parseResult.FullSeason)
{
switch (Path.GetExtension(filename).ToLowerInvariant())
{
case ".xml":
metadata.Type = MetadataType.EpisodeMetadata;
return metadata;
case ".jpg":
metadata.Type = MetadataType.EpisodeImage;
return metadata;
}
}
return null;
}
private MetadataFile WriteSeriesImages(Series series, List<MetadataFile> existingMetadataFiles)
{
//Because we only support one image, attempt to get the Poster type, then if that fails grab the first
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
if (image == null)
{
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
return null;
}
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
var destination = Path.Combine(series.Path, Path.GetFileName(series.Path) + Path.GetExtension(source));
//TODO: Do we want to overwrite the file if it exists?
if (_diskProvider.FileExists(destination))
{
_logger.Debug("Series image: {0} already exists.", image.CoverType);
return null;
}
else
{
_diskProvider.CopyFile(source, destination, false);
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ??
new MetadataFile
{
SeriesId = series.Id,
Consumer = GetType().Name,
Type = MetadataType.SeriesImage,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination)
};
return metadata;
}
}
private IEnumerable<MetadataFile> WriteSeasonImages(Series series, List<MetadataFile> existingMetadataFiles)
{
_logger.Debug("Writing season images for {0}.", series.Title);
//Create a dictionary between season number and output folder
var seasonFolderMap = new Dictionary<int, string>();
foreach (var folder in Directory.EnumerateDirectories(series.Path))
{
var directoryinfo = new DirectoryInfo(folder);
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
if (seasonMatch.Success)
{
var seasonNumber = seasonMatch.Groups["season"].Value;
if (seasonNumber.Contains("specials"))
{
seasonFolderMap[0] = folder;
}
else
{
int matchedSeason;
if (Int32.TryParse(seasonNumber, out matchedSeason))
{
seasonFolderMap[matchedSeason] = folder;
}
else
{
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
}
}
}
else
{
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
}
}
foreach (var season in series.Seasons)
{
//Work out the path to this season - if we don't have a matching path then skip this season.
string seasonFolder;
if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder))
{
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
continue;
}
//Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
if (image == null)
{
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
continue;
}
var filename = Path.GetFileName(seasonFolder) + ".jpg";
var path = Path.Combine(series.Path, seasonFolder, filename);
_logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path);
DownloadImage(series, image.Url, path);
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
c.SeasonNumber == season.SeasonNumber) ??
new MetadataFile
{
SeriesId = series.Id,
SeasonNumber = season.SeasonNumber,
Consumer = GetType().Name,
Type = MetadataType.SeasonImage,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
};
yield return metadata;
}
}
private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
{
var filename = GetEpisodeMetadataFilename(episodeFile.Path);
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata &&
c.EpisodeFileId == episodeFile.Id);
if (existingMetadata != null)
{
var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
if (!filename.PathEquals(fullPath))
{
_diskProvider.MoveFile(fullPath, filename);
existingMetadata.RelativePath = relativePath;
}
}
_logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path);
var xmlResult = String.Empty;
foreach (var episode in episodeFile.Episodes.Value)
{
var sb = new StringBuilder();
var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true;
xws.Indent = false;
using (var xw = XmlWriter.Create(sb, xws))
{
var doc = new XDocument();
var details = new XElement("video");
details.Add(new XElement("title", String.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title)));
details.Add(new XElement("year", episode.AirDate));
details.Add(new XElement("genre", String.Join(" / ", series.Genres)));
var actors = String.Join(" , ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character).GetRange(0, Math.Min(3, series.Actors.Count)));
details.Add(new XElement("actors", actors));
details.Add(new XElement("description", episode.Overview));
details.Add(new XElement("length", series.Runtime));
details.Add(new XElement("mpaa", ValidCertification.Contains( series.Certification.ToUpperInvariant() ) ? series.Certification.ToUpperInvariant() : "UNRATED" ) );
doc.Add(details);
doc.Save(xw);
xmlResult += doc.ToString();
xmlResult += Environment.NewLine;
}
}
_logger.Debug("Saving episodedetails to: {0}", filename);
_diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
var metadata = existingMetadata ??
new MetadataFile
{
SeriesId = series.Id,
EpisodeFileId = episodeFile.Id,
Consumer = GetType().Name,
Type = MetadataType.EpisodeMetadata,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
};
return metadata;
}
private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
{
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
if (screenshot == null)
{
_logger.Trace("Episode screenshot not available");
return null;
}
var filename = GetEpisodeImageFilename(episodeFile.Path);
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage &&
c.EpisodeFileId == episodeFile.Id);
if (existingMetadata != null)
{
var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
if (!filename.PathEquals(fullPath))
{
_diskProvider.MoveFile(fullPath, filename);
existingMetadata.RelativePath = relativePath;
}
}
DownloadImage(series, screenshot.Url, filename);
var metadata = existingMetadata ??
new MetadataFile
{
SeriesId = series.Id,
EpisodeFileId = episodeFile.Id,
Consumer = GetType().Name,
Type = MetadataType.EpisodeImage,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
};
return metadata;
}
private string GetEpisodeMetadataFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "xml");
}
private string GetEpisodeImageFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "jpg");
}
}
}

View File

@ -0,0 +1,53 @@
using System;
using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Metadata.Consumers.Roksbox
{
public class RoksboxSettingsValidator : AbstractValidator<RoksboxMetadataSettings>
{
public RoksboxSettingsValidator()
{
}
}
public class RoksboxMetadataSettings : IProviderConfig
{
private static readonly RoksboxSettingsValidator Validator = new RoksboxSettingsValidator();
public RoksboxMetadataSettings()
{
EpisodeMetadata = true;
SeriesImages = true;
SeasonImages = true;
EpisodeImages = true;
}
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)]
public Boolean EpisodeMetadata { get; set; }
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)]
public Boolean SeriesImages { get; set; }
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)]
public Boolean SeasonImages { get; set; }
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)]
public Boolean EpisodeImages { get; set; }
public bool IsValid
{
get
{
return true;
}
}
public ValidationResult Validate()
{
return Validator.Validate(this);
}
}
}

View File

@ -0,0 +1,475 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.Wdtv
{
public class WdtvMetadata : MetadataBase<WdtvMetadataSettings>
{
private readonly IEventAggregator _eventAggregator;
private readonly IMapCoversToLocal _mediaCoverService;
private readonly IMediaFileService _mediaFileService;
private readonly IMetadataFileService _metadataFileService;
private readonly IDiskProvider _diskProvider;
private readonly IHttpProvider _httpProvider;
private readonly IEpisodeService _episodeService;
private readonly Logger _logger;
public WdtvMetadata(IEventAggregator eventAggregator,
IMapCoversToLocal mediaCoverService,
IMediaFileService mediaFileService,
IMetadataFileService metadataFileService,
IDiskProvider diskProvider,
IHttpProvider httpProvider,
IEpisodeService episodeService,
Logger logger)
: base(diskProvider, httpProvider, logger)
{
_eventAggregator = eventAggregator;
_mediaCoverService = mediaCoverService;
_mediaFileService = mediaFileService;
_metadataFileService = metadataFileService;
_diskProvider = diskProvider;
_httpProvider = httpProvider;
_episodeService = episodeService;
_logger = logger;
}
private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<season>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
{
var metadataFiles = new List<MetadataFile>();
if (!_diskProvider.FolderExists(series.Path))
{
_logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path);
return;
}
if (Settings.SeriesImages)
{
var metadata = WriteSeriesImages(series, existingMetadataFiles);
if (metadata != null)
{
metadataFiles.Add(metadata);
}
}
if (Settings.SeasonImages)
{
var metadata = WriteSeasonImages(series, existingMetadataFiles);
if (metadata != null)
{
metadataFiles.AddRange(metadata);
}
}
foreach (var episodeFile in episodeFiles)
{
if (Settings.EpisodeMetadata)
{
var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles);
if (metadata != null)
{
metadataFiles.Add(metadata);
}
}
}
foreach (var episodeFile in episodeFiles)
{
if (Settings.EpisodeImages)
{
var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles);
if (metadataFile != null)
{
metadataFiles.Add(metadataFile);
}
}
}
metadataFiles.RemoveAll(c => c == null);
_eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
}
public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
{
var metadataFiles = new List<MetadataFile>();
if (Settings.EpisodeMetadata)
{
metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List<MetadataFile>()));
}
if (Settings.EpisodeImages)
{
var metadataFile = WriteEpisodeImages(series, episodeFile, new List<MetadataFile>());
if (metadataFile != null)
{
metadataFiles.Add(metadataFile);
}
}
_eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
}
public override void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
{
var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
var updatedMetadataFiles = new List<MetadataFile>();
foreach (var episodeFile in episodeFiles)
{
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
foreach (var metadataFile in metadataFiles)
{
string newFilename;
if (metadataFile.Type == MetadataType.EpisodeImage)
{
newFilename = GetEpisodeImageFilename(episodeFile.Path);
}
else if (metadataFile.Type == MetadataType.EpisodeMetadata)
{
newFilename = GetEpisodeMetadataFilename(episodeFile.Path);
}
else
{
_logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath);
continue;
}
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath);
if (!newFilename.PathEquals(existingFilename))
{
_diskProvider.MoveFile(existingFilename, newFilename);
metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename);
updatedMetadataFiles.Add(metadataFile);
}
}
}
_eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles));
}
public override MetadataFile FindMetadataFile(Series series, string path)
{
var filename = Path.GetFileName(path);
if (filename == null) return null;
var metadata = new MetadataFile
{
SeriesId = series.Id,
Consumer = GetType().Name,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
};
//Series and season images are both named folder.jpg, only season ones sit in season folders
if (String.Compare(filename, "folder.jpg", true) == 0)
{
var parentdir = Directory.GetParent(path);
var seasonMatch = SeasonImagesRegex.Match(parentdir.Name);
if (seasonMatch.Success)
{
metadata.Type = MetadataType.SeasonImage;
var seasonNumber = seasonMatch.Groups["season"].Value;
if (seasonNumber.Contains("specials"))
{
metadata.SeasonNumber = 0;
}
else
{
metadata.SeasonNumber = Convert.ToInt32(seasonNumber);
}
return metadata;
}
else
{
metadata.Type = MetadataType.SeriesImage;
return metadata;
}
}
var parseResult = Parser.Parser.ParseTitle(filename);
if (parseResult != null &&
!parseResult.FullSeason)
{
switch (Path.GetExtension(filename).ToLowerInvariant())
{
case ".xml":
metadata.Type = MetadataType.EpisodeMetadata;
return metadata;
case ".metathumb":
metadata.Type = MetadataType.EpisodeImage;
return metadata;
}
}
return null;
}
private MetadataFile WriteSeriesImages(Series series, List<MetadataFile> existingMetadataFiles)
{
//Because we only support one image, attempt to get the Poster type, then if that fails grab the first
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
if (image == null)
{
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
return null;
}
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
var destination = Path.Combine(series.Path, "folder" + Path.GetExtension(source));
//TODO: Do we want to overwrite the file if it exists?
if (_diskProvider.FileExists(destination))
{
_logger.Debug("Series image: {0} already exists.", image.CoverType);
return null;
}
else
{
_diskProvider.CopyFile(source, destination, false);
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ??
new MetadataFile
{
SeriesId = series.Id,
Consumer = GetType().Name,
Type = MetadataType.SeriesImage,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination)
};
return metadata;
}
}
private IEnumerable<MetadataFile> WriteSeasonImages(Series series, List<MetadataFile> existingMetadataFiles)
{
_logger.Debug("Writing season images for {0}.", series.Title);
//Create a dictionary between season number and output folder
var seasonFolderMap = new Dictionary<int, string>();
foreach (var folder in Directory.EnumerateDirectories(series.Path))
{
var directoryinfo = new DirectoryInfo(folder);
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
if (seasonMatch.Success)
{
var seasonNumber = seasonMatch.Groups["season"].Value;
if (seasonNumber.Contains("specials"))
{
seasonFolderMap[0] = folder;
}
else
{
int matchedSeason;
if (Int32.TryParse(seasonNumber, out matchedSeason))
{
seasonFolderMap[matchedSeason] = folder;
}
else
{
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
}
}
}
else
{
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
}
}
foreach (var season in series.Seasons)
{
//Work out the path to this season - if we don't have a matching path then skip this season.
string seasonFolder;
if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder))
{
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
continue;
}
//WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
if (image == null)
{
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
continue;
}
var filename = "folder.jpg";
var path = Path.Combine(series.Path, seasonFolder, filename);
_logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path);
DownloadImage(series, image.Url, path);
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
c.SeasonNumber == season.SeasonNumber) ??
new MetadataFile
{
SeriesId = series.Id,
SeasonNumber = season.SeasonNumber,
Consumer = GetType().Name,
Type = MetadataType.SeasonImage,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
};
yield return metadata;
}
}
private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
{
var filename = GetEpisodeMetadataFilename(episodeFile.Path);
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata &&
c.EpisodeFileId == episodeFile.Id);
if (existingMetadata != null)
{
var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
if (!filename.PathEquals(fullPath))
{
_diskProvider.MoveFile(fullPath, filename);
existingMetadata.RelativePath = relativePath;
}
}
_logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path);
var xmlResult = String.Empty;
foreach (var episode in episodeFile.Episodes.Value)
{
var sb = new StringBuilder();
var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true;
xws.Indent = false;
using (var xw = XmlWriter.Create(sb, xws))
{
var doc = new XDocument();
var details = new XElement("details");
details.Add(new XElement("id", series.Id));
details.Add(new XElement("title", String.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title)));
details.Add(new XElement("series_name", series.Title));
details.Add(new XElement("episode_name", episode.Title));
details.Add(new XElement("season_number", episode.SeasonNumber));
details.Add(new XElement("episode_number", episode.EpisodeNumber));
details.Add(new XElement("firstaired", episode.AirDate));
details.Add(new XElement("genre", String.Join(" / ", series.Genres)));
details.Add(new XElement("actor", String.Join(" / ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character))));
details.Add(new XElement("overview", episode.Overview));
//Todo: get guest stars, writer and director
//details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault()));
//details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault()));
doc.Add(details);
doc.Save(xw);
xmlResult += doc.ToString();
xmlResult += Environment.NewLine;
}
}
_logger.Debug("Saving episodedetails to: {0}", filename);
_diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
var metadata = existingMetadata ??
new MetadataFile
{
SeriesId = series.Id,
EpisodeFileId = episodeFile.Id,
Consumer = GetType().Name,
Type = MetadataType.EpisodeMetadata,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
};
return metadata;
}
private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
{
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
if (screenshot == null)
{
_logger.Trace("Episode screenshot not available");
return null;
}
var filename = GetEpisodeImageFilename(episodeFile.Path);
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage &&
c.EpisodeFileId == episodeFile.Id);
if (existingMetadata != null)
{
var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
if (!filename.PathEquals(fullPath))
{
_diskProvider.MoveFile(fullPath, filename);
existingMetadata.RelativePath = relativePath;
}
}
DownloadImage(series, screenshot.Url, filename);
var metadata = existingMetadata ??
new MetadataFile
{
SeriesId = series.Id,
EpisodeFileId = episodeFile.Id,
Consumer = GetType().Name,
Type = MetadataType.EpisodeImage,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
};
return metadata;
}
private string GetEpisodeMetadataFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "xml");
}
private string GetEpisodeImageFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "metathumb");
}
}
}

View File

@ -0,0 +1,53 @@
using System;
using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Metadata.Consumers.Wdtv
{
public class WdtvSettingsValidator : AbstractValidator<WdtvMetadataSettings>
{
public WdtvSettingsValidator()
{
}
}
public class WdtvMetadataSettings : IProviderConfig
{
private static readonly WdtvSettingsValidator Validator = new WdtvSettingsValidator();
public WdtvMetadataSettings()
{
EpisodeMetadata = true;
SeriesImages = true;
SeasonImages = true;
EpisodeImages = true;
}
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)]
public Boolean EpisodeMetadata { get; set; }
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)]
public Boolean SeriesImages { get; set; }
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)]
public Boolean SeasonImages { get; set; }
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)]
public Boolean EpisodeImages { get; set; }
public bool IsValid
{
get
{
return true;
}
}
public ValidationResult Validate()
{
return Validator.Validate(this);
}
}
}

View File

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml; using System.Xml;
@ -10,7 +9,6 @@ using System.Xml.Linq;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;

View File

@ -4,7 +4,6 @@ using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Metadata.Consumers.Fake;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Metadata namespace NzbDrone.Core.Metadata
@ -30,8 +29,6 @@ namespace NzbDrone.Core.Metadata
foreach (var provider in _providers) foreach (var provider in _providers)
{ {
if (provider.GetType() == typeof(FakeMetadata)) continue;;
definitions.Add(new MetadataDefinition definitions.Add(new MetadataDefinition
{ {
Enable = false, Enable = false,

View File

@ -236,6 +236,8 @@
<Compile Include="Download\Clients\Blackhole\TestBlackholeCommand.cs" /> <Compile Include="Download\Clients\Blackhole\TestBlackholeCommand.cs" />
<Compile Include="Download\Clients\DownloadClientException.cs" /> <Compile Include="Download\Clients\DownloadClientException.cs" />
<Compile Include="Download\Clients\FolderSettings.cs" /> <Compile Include="Download\Clients\FolderSettings.cs" />
<Compile Include="Download\Clients\Nzbget\NzbgetHistoryItem.cs" />
<Compile Include="Download\Clients\Nzbget\NzbgetParameter.cs" />
<Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" />
<Compile Include="Download\Clients\Nzbget\TestNzbgetCommand.cs" /> <Compile Include="Download\Clients\Nzbget\TestNzbgetCommand.cs" />
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" /> <Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
@ -280,12 +282,14 @@
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFiles.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" /> <Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" /> <Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" />
<Compile Include="Housekeeping\HousekeepingCommand.cs" /> <Compile Include="Housekeeping\HousekeepingCommand.cs" />
<Compile Include="Housekeeping\HousekeepingService.cs" /> <Compile Include="Housekeeping\HousekeepingService.cs" />
<Compile Include="Housekeeping\IHousekeepingTask.cs" /> <Compile Include="Housekeeping\IHousekeepingTask.cs" />
<Compile Include="IndexerSearch\MissingEpisodeSearchCommand.cs" />
<Compile Include="IndexerSearch\Definitions\SpecialEpisodeSearchCriteria.cs" /> <Compile Include="IndexerSearch\Definitions\SpecialEpisodeSearchCriteria.cs" />
<Compile Include="IndexerSearch\SeriesSearchService.cs" /> <Compile Include="IndexerSearch\SeriesSearchService.cs" />
<Compile Include="IndexerSearch\SeriesSearchCommand.cs" /> <Compile Include="IndexerSearch\SeriesSearchCommand.cs" />
@ -296,6 +300,7 @@
<Compile Include="Indexers\BasicTorrentRssParser.cs" /> <Compile Include="Indexers\BasicTorrentRssParser.cs" />
<Compile Include="Indexers\DownloadProtocols.cs" /> <Compile Include="Indexers\DownloadProtocols.cs" />
<Compile Include="Indexers\Exceptions\ApiKeyException.cs" /> <Compile Include="Indexers\Exceptions\ApiKeyException.cs" />
<Compile Include="Indexers\Exceptions\RequestLimitReachedException.cs" />
<Compile Include="Indexers\Eztv\Eztv.cs" /> <Compile Include="Indexers\Eztv\Eztv.cs" />
<Compile Include="Indexers\FetchAndParseRssService.cs" /> <Compile Include="Indexers\FetchAndParseRssService.cs" />
<Compile Include="Indexers\IIndexer.cs" /> <Compile Include="Indexers\IIndexer.cs" />
@ -341,9 +346,11 @@
<Compile Include="MetadataSource\Trakt\Actor.cs" /> <Compile Include="MetadataSource\Trakt\Actor.cs" />
<Compile Include="MetadataSource\Trakt\People.cs" /> <Compile Include="MetadataSource\Trakt\People.cs" />
<Compile Include="MetadataSource\Trakt\Ratings.cs" /> <Compile Include="MetadataSource\Trakt\Ratings.cs" />
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadata.cs" />
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadataSettings.cs" />
<Compile Include="Metadata\Consumers\Wdtv\WdtvMetadata.cs" />
<Compile Include="Metadata\Consumers\Wdtv\WdtvMetadataSettings.cs" />
<Compile Include="Metadata\Files\CleanMetadataService.cs" /> <Compile Include="Metadata\Files\CleanMetadataService.cs" />
<Compile Include="Metadata\Consumers\Fake\Fake.cs" />
<Compile Include="Metadata\Consumers\Fake\FakeSettings.cs" />
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" /> <Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" />
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" /> <Compile Include="Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" />
<Compile Include="Metadata\ExistingMetadataService.cs" /> <Compile Include="Metadata\ExistingMetadataService.cs" />
@ -498,10 +505,10 @@
<Compile Include="Instrumentation\LogService.cs" /> <Compile Include="Instrumentation\LogService.cs" />
<Compile Include="Instrumentation\DatabaseTarget.cs" /> <Compile Include="Instrumentation\DatabaseTarget.cs" />
<Compile Include="MediaFiles\MediaInfo\MediaInfoModel.cs" /> <Compile Include="MediaFiles\MediaInfo\MediaInfoModel.cs" />
<Compile Include="Download\Clients\Nzbget\EnqueueResponse.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetBooleanResponse.cs" />
<Compile Include="Download\Clients\Nzbget\ErrorModel.cs" /> <Compile Include="Download\Clients\Nzbget\ErrorModel.cs" />
<Compile Include="Download\Clients\Nzbget\JsonError.cs" /> <Compile Include="Download\Clients\Nzbget\JsonError.cs" />
<Compile Include="Download\Clients\Nzbget\NzbgetQueue.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetListResponse.cs" />
<Compile Include="Download\Clients\Nzbget\NzbgetQueueItem.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetQueueItem.cs" />
<Compile Include="Download\Clients\Nzbget\NzbgetPriority.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetPriority.cs" />
<Compile Include="Download\Clients\Nzbget\VersionResponse.cs" /> <Compile Include="Download\Clients\Nzbget\VersionResponse.cs" />
@ -663,6 +670,7 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Tv\SeriesStatusType.cs" /> <Compile Include="Tv\SeriesStatusType.cs" />
<Compile Include="Tv\RefreshSeriesService.cs" /> <Compile Include="Tv\RefreshSeriesService.cs" />
<Compile Include="Tv\ShouldRefreshSeries.cs" />
<Compile Include="Update\Commands\ApplicationUpdateCommand.cs" /> <Compile Include="Update\Commands\ApplicationUpdateCommand.cs" />
<Compile Include="Update\Commands\InstallUpdateCommand.cs" /> <Compile Include="Update\Commands\InstallUpdateCommand.cs" />
<Compile Include="Update\InstallUpdateService.cs" /> <Compile Include="Update\InstallUpdateService.cs" />

View File

@ -102,7 +102,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a|an|the|and|or|of)(?:\b|_))|\W|_", private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a|an|the|and|or|of)(?:\b|_))|\W|_",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleTitleRegex = new Regex(@"480[i|p]|720[i|p]|1080[i|p]|[x|h|x\s|h\s]264|DD\W?5\W1|\<|\>|\?|\*|\:|\|", private static readonly Regex SimpleTitleRegex = new Regex(@"480[i|p]|720[i|p]|1080[i|p]|[xh][\W_]?264|DD\W?5\W1|\<|\>|\?|\*|\:|\|",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex MultiPartCleanupRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled); private static readonly Regex MultiPartCleanupRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled);

View File

@ -31,7 +31,17 @@ namespace NzbDrone.Core.RootFolders
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly Logger _logger; private readonly Logger _logger;
private static readonly HashSet<string> SpecialFolders = new HashSet<string> { "$recycle.bin", "system volume information", "recycler", "lost+found" }; private static readonly HashSet<string> SpecialFolders = new HashSet<string>
{
"$recycle.bin",
"system volume information",
"recycler",
"lost+found",
".appledb",
".appledesktop",
".appledouble",
"@eadir"
};
public RootFolderService(IRootFolderRepository rootFolderRepository, public RootFolderService(IRootFolderRepository rootFolderRepository,
@ -123,11 +133,8 @@ namespace NzbDrone.Core.RootFolders
results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName }); results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName });
} }
if (Path.GetPathRoot(path).Equals(path, StringComparison.InvariantCultureIgnoreCase))
{
var setToRemove = SpecialFolders; var setToRemove = SpecialFolders;
results.RemoveAll(x => setToRemove.Contains(new DirectoryInfo(x.Path.ToLowerInvariant()).Name)); results.RemoveAll(x => setToRemove.Contains(new DirectoryInfo(x.Path.ToLowerInvariant()).Name));
}
Logger.Debug("{0} unmapped folders detected.", results.Count); Logger.Debug("{0} unmapped folders detected.", results.Count);
return results; return results;

View File

@ -5,6 +5,8 @@ using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.DataAugmentation.DailySeries; using NzbDrone.Core.DataAugmentation.DailySeries;
using NzbDrone.Core.Instrumentation.Extensions; using NzbDrone.Core.Instrumentation.Extensions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
@ -21,15 +23,26 @@ namespace NzbDrone.Core.Tv
private readonly IRefreshEpisodeService _refreshEpisodeService; private readonly IRefreshEpisodeService _refreshEpisodeService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IDailySeriesService _dailySeriesService; private readonly IDailySeriesService _dailySeriesService;
private readonly IDiskScanService _diskScanService;
private readonly ICheckIfSeriesShouldBeRefreshed _checkIfSeriesShouldBeRefreshed;
private readonly Logger _logger; private readonly Logger _logger;
public RefreshSeriesService(IProvideSeriesInfo seriesInfo, ISeriesService seriesService, IRefreshEpisodeService refreshEpisodeService, IEventAggregator eventAggregator, IDailySeriesService dailySeriesService, Logger logger) public RefreshSeriesService(IProvideSeriesInfo seriesInfo,
ISeriesService seriesService,
IRefreshEpisodeService refreshEpisodeService,
IEventAggregator eventAggregator,
IDailySeriesService dailySeriesService,
IDiskScanService diskScanService,
ICheckIfSeriesShouldBeRefreshed checkIfSeriesShouldBeRefreshed,
Logger logger)
{ {
_seriesInfo = seriesInfo; _seriesInfo = seriesInfo;
_seriesService = seriesService; _seriesService = seriesService;
_refreshEpisodeService = refreshEpisodeService; _refreshEpisodeService = refreshEpisodeService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_dailySeriesService = dailySeriesService; _dailySeriesService = dailySeriesService;
_diskScanService = diskScanService;
_checkIfSeriesShouldBeRefreshed = checkIfSeriesShouldBeRefreshed;
_logger = logger; _logger = logger;
} }
@ -115,6 +128,8 @@ namespace NzbDrone.Core.Tv
var allSeries = _seriesService.GetAllSeries().OrderBy(c => c.Title).ToList(); var allSeries = _seriesService.GetAllSeries().OrderBy(c => c.Title).ToList();
foreach (var series in allSeries) foreach (var series in allSeries)
{
if (_checkIfSeriesShouldBeRefreshed.ShouldRefresh(series))
{ {
try try
{ {
@ -125,6 +140,20 @@ namespace NzbDrone.Core.Tv
_logger.ErrorException("Couldn't refresh info for {0}".Inject(series), e); _logger.ErrorException("Couldn't refresh info for {0}".Inject(series), e);
} }
} }
else
{
try
{
_logger.Info("Skipping refresh of series: {0}", series.Title);
_diskScanService.Scan(series);
}
catch (Exception e)
{
_logger.ErrorException("Couldn't rescan series {0}".Inject(series), e);
}
}
}
} }
} }

View File

@ -0,0 +1,49 @@
using System;
using System.Linq;
using NLog;
namespace NzbDrone.Core.Tv
{
public interface ICheckIfSeriesShouldBeRefreshed
{
bool ShouldRefresh(Series series);
}
public class ShouldRefreshSeries : ICheckIfSeriesShouldBeRefreshed
{
private readonly IEpisodeService _episodeService;
private readonly Logger _logger;
public ShouldRefreshSeries(IEpisodeService episodeService, Logger logger)
{
_episodeService = episodeService;
_logger = logger;
}
public bool ShouldRefresh(Series series)
{
if (series.Status == SeriesStatusType.Continuing)
{
_logger.Trace("Series {0} is continuing, should refresh.", series.Title);
return true;
}
if (series.LastInfoSync < DateTime.UtcNow.AddDays(-30))
{
_logger.Trace("Series {0} last updated more than 30 days ago, should refresh.", series.Title);
return true;
}
var lastEpisode = _episodeService.GetEpisodeBySeries(series.Id).OrderByDescending(e => e.AirDateUtc).FirstOrDefault();
if (lastEpisode != null && lastEpisode.AirDateUtc > DateTime.UtcNow.AddDays(-30))
{
_logger.Trace("Last episode in {0} aired less than 30 days ago, should refresh.", series.Title);
return true;
}
_logger.Trace("Series {0} should not be refreshed.", series.Title);
return false;
}
}
}

View File

@ -43,6 +43,12 @@ namespace NzbDrone.Host.AccessControl
var localHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort); var localHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort);
var wildcardHttpsUrls = BuildUrls("https", "*", _configFileProvider.SslPort); var wildcardHttpsUrls = BuildUrls("https", "*", _configFileProvider.SslPort);
if (!_configFileProvider.EnableSsl)
{
localHttpsUrls.Clear();
wildcardHttpsUrls.Clear();
}
if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin) if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin)
{ {
var httpUrls = wildcardHttpUrls.All(IsRegistered) ? wildcardHttpUrls : localHttpUrls; var httpUrls = wildcardHttpUrls.All(IsRegistered) ? wildcardHttpUrls : localHttpUrls;
@ -51,7 +57,6 @@ namespace NzbDrone.Host.AccessControl
Urls.AddRange(httpUrls); Urls.AddRange(httpUrls);
Urls.AddRange(httpsUrls); Urls.AddRange(httpsUrls);
} }
else else
{ {
Urls.AddRange(wildcardHttpUrls); Urls.AddRange(wildcardHttpUrls);

View File

@ -148,6 +148,9 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\nuget.targets" /> <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
<PropertyGroup> <PropertyGroup>

View File

@ -76,7 +76,9 @@
<Name>NzbDrone.Test.Common</Name> <Name>NzbDrone.Test.Common</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\nuget.targets" /> <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -100,6 +100,9 @@
<Name>NzbDrone.Test.Common</Name> <Name>NzbDrone.Test.Common</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" /> <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -87,6 +87,9 @@
<Name>NzbDrone.Update</Name> <Name>NzbDrone.Update</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\nuget.targets" /> <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -101,6 +101,9 @@
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" /> <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -8,16 +8,16 @@
<option es3="false" /> <option es3="false" />
<option forin="true" /> <option forin="true" />
<option immed="true" /> <option immed="true" />
<option latedef="true" />
<option newcap="true" /> <option newcap="true" />
<option noarg="true" /> <option noarg="true" />
<option noempty="false" /> <option noempty="false" />
<option nonew="true" /> <option nonew="true" />
<option plusplus="false" /> <option plusplus="false" />
<option undef="true" /> <option undef="true" />
<option unused="true" />
<option strict="true" /> <option strict="true" />
<option trailing="false" /> <option trailing="false" />
<option latedef="true" />
<option unused="true" />
<option quotmark="single" /> <option quotmark="single" />
<option maxdepth="3" /> <option maxdepth="3" />
<option asi="false" /> <option asi="false" />

View File

@ -0,0 +1,25 @@
'use strict';
define(
[
'marionette',
'System/StatusModel',
'Mixins/CopyToClipboard'
], function (Marionette, StatusModel) {
return Marionette.Layout.extend({
template: 'Calendar/CalendarFeedViewTemplate',
ui: {
icalUrl : '.x-ical-url',
icalCopy : '.x-ical-copy'
},
templateHelpers: {
icalHttpUrl : window.location.protocol + '//' + window.location.host + StatusModel.get('urlBase') + '/feed/calendar/NzbDrone.ics',
icalWebCalUrl : 'webcal://' + window.location.host + StatusModel.get('urlBase') + '/feed/calendar/NzbDrone.ics'
},
onShow: function () {
this.ui.icalCopy.copyToClipboard(this.ui.icalUrl);
}
});
});

View File

@ -0,0 +1,29 @@
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>NzbDrone Calendar feed</h3>
</div>
<div class="modal-body edit-series-modal">
<div class="row">
<div>
<div class="form-horizontal">
<div class="control-group">
<label class="control-label">iCal feed</label>
<div class="controls ical-url">
<div class="input-append">
<input type="text" class="x-ical-url" value="{{icalHttpUrl}}" readonly="readonly" />
<button class="btn btn-icon-only x-ical-copy" title="Copy to clipboard"><i class="icon-copy"></i></button>
<a class="btn btn-icon-only no-router" title="Subscribe" href="{{icalWebCalUrl}}" target="_blank"><i class="icon-calendar-empty"></i></a>
</div>
<span class="help-inline">
<i class="icon-nd-form-info" title="Copy this url into your clients subscription form or use the subscribe button if your browser support webcal"/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal">close</button>
</div>

View File

@ -1,10 +1,12 @@
'use strict'; 'use strict';
define( define(
[ [
'AppLayout',
'marionette', 'marionette',
'Calendar/UpcomingCollectionView', 'Calendar/UpcomingCollectionView',
'Calendar/CalendarView' 'Calendar/CalendarView',
], function (Marionette, UpcomingCollectionView, CalendarView) { 'Calendar/CalendarFeedView'
], function (AppLayout, Marionette, UpcomingCollectionView, CalendarView, CalendarFeedView) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'Calendar/CalendarLayoutTemplate', template: 'Calendar/CalendarLayoutTemplate',
@ -13,6 +15,10 @@ define(
calendar: '#x-calendar' calendar: '#x-calendar'
}, },
events: {
'click .x-ical': '_showiCal'
},
onShow: function () { onShow: function () {
this._showUpcoming(); this._showUpcoming();
this._showCalendar(); this._showCalendar();
@ -24,6 +30,11 @@ define(
_showCalendar: function () { _showCalendar: function () {
this.calendar.show(new CalendarView()); this.calendar.show(new CalendarView());
},
_showiCal: function () {
var view = new CalendarFeedView();
AppLayout.modalRegion.show(view);
} }
}); });
}); });

View File

@ -1,6 +1,13 @@
<div class="row"> <div class="row">
<div class="span3"> <div class="span3">
<div class="pull-left">
<h4>Upcoming</h4> <h4>Upcoming</h4>
</div>
<div class="pull-right">
<h4>
<i class="icon-calendar-empty ical x-ical"></i>
</h4>
</div>
<div id="x-upcoming"/> <div id="x-upcoming"/>
</div> </div>
<div class=span9> <div class=span9>

View File

@ -8,25 +8,24 @@ define(
'Calendar/Collection', 'Calendar/Collection',
'System/StatusModel', 'System/StatusModel',
'History/Queue/QueueCollection', 'History/Queue/QueueCollection',
'Config',
'Mixins/backbone.signalr.mixin', 'Mixins/backbone.signalr.mixin',
'fullcalendar', 'fullcalendar',
'jquery.easypiechart' 'jquery.easypiechart'
], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection) { ], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection, Config) {
var _instance;
return Marionette.ItemView.extend({ return Marionette.ItemView.extend({
storageKey: 'calendar.view',
initialize: function () { initialize: function () {
this.collection = new CalendarCollection().bindSignalR({ updateOnly: true }); this.collection = new CalendarCollection().bindSignalR({ updateOnly: true });
this.listenTo(this.collection, 'change', this._reloadCalendarEvents); this.listenTo(this.collection, 'change', this._reloadCalendarEvents);
this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents); this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents);
}, },
render : function () { render : function () {
var self = this;
this.$el.empty().fullCalendar({ this.$el.empty().fullCalendar({
defaultView : 'basicWeek', defaultView : Config.getValue(this.storageKey, 'basicWeek'),
allDayDefault : false, allDayDefault : false,
ignoreTimezone: false, ignoreTimezone: false,
weekMode : 'variable', weekMode : 'variable',
@ -41,16 +40,35 @@ define(
prev: '<i class="icon-arrow-left"></i>', prev: '<i class="icon-arrow-left"></i>',
next: '<i class="icon-arrow-right"></i>' next: '<i class="icon-arrow-right"></i>'
}, },
viewRender : this._getEvents, viewRender : this._viewRender.bind(this),
eventRender : function (event, element) { eventRender : this._eventRender.bind(this),
self.$(element).addClass(event.statusLevel); eventClick : function (event) {
self.$(element).children('.fc-event-inner').addClass(event.statusLevel); vent.trigger(vent.Commands.ShowEpisodeDetails, {episode: event.model});
}
});
},
onShow: function () {
this.$('.fc-button-today').click();
},
_viewRender: function (view) {
if (Config.getValue(this.storageKey) !== view.name) {
Config.setValue(this.storageKey, view.name);
}
this._getEvents(view);
},
_eventRender: function (event, element) {
this.$(element).addClass(event.statusLevel);
this.$(element).children('.fc-event-inner').addClass(event.statusLevel);
if (event.progress > 0) { if (event.progress > 0) {
self.$(element).find('.fc-event-time') this.$(element).find('.fc-event-time')
.after('<span class="chart pull-right" data-percent="{0}"></span>'.format(event.progress)); .after('<span class="chart pull-right" data-percent="{0}"></span>'.format(event.progress));
self.$(element).find('.chart').easyPieChart({ this.$(element).find('.chart').easyPieChart({
barColor : '#ffffff', barColor : '#ffffff',
trackColor: false, trackColor: false,
scaleColor: false, scaleColor: false,
@ -60,35 +78,24 @@ define(
}); });
} }
}, },
eventClick : function (event) {
vent.trigger(vent.Commands.ShowEpisodeDetails, {episode: event.model});
}
});
_instance = this;
},
onShow: function () {
this.$('.fc-button-today').click();
},
_getEvents: function (view) { _getEvents: function (view) {
var start = moment(view.visStart).toISOString(); var start = moment(view.visStart).toISOString();
var end = moment(view.visEnd).toISOString(); var end = moment(view.visEnd).toISOString();
_instance.$el.fullCalendar('removeEvents'); this.$el.fullCalendar('removeEvents');
_instance.collection.fetch({ this.collection.fetch({
data : { start: start, end: end }, data : { start: start, end: end },
success: function (collection) { success: this._setEventData.bind(this)
_instance._setEventData(collection);
}
}); });
}, },
_setEventData: function (collection) { _setEventData: function (collection) {
var events = []; var events = [];
var self = this;
collection.each(function (model) { collection.each(function (model) {
var seriesTitle = model.get('series').title; var seriesTitle = model.get('series').title;
var start = model.get('airDateUtc'); var start = model.get('airDateUtc');
@ -100,15 +107,15 @@ define(
start : start, start : start,
end : end, end : end,
allDay : false, allDay : false,
statusLevel : _instance._getStatusLevel(model, end), statusLevel : self._getStatusLevel(model, end),
progress : _instance._getDownloadProgress(model), progress : self._getDownloadProgress(model),
model : model model : model
}; };
events.push(event); events.push(event);
}); });
_instance.$el.fullCalendar('addEventSource', events); this.$el.fullCalendar('addEventSource', events);
}, },
_getStatusLevel: function (element, endTime) { _getStatusLevel: function (element, endTime) {

View File

@ -158,3 +158,17 @@
margin-right: 2px; margin-right: 2px;
} }
} }
.ical
{
color: @btnInverseBackground;
cursor: pointer;
}
.ical-url {
input {
width : 440px;
cursor : text;
}
}

View File

@ -18,7 +18,7 @@ define(
mode: 'client', mode: 'client',
findEpisode: function (episodeId) { findEpisode: function (episodeId) {
return _.find(this.models, function (queueModel) { return _.find(this.fullCollection.models, function (queueModel) {
return queueModel.get('episode').id === episodeId; return queueModel.get('episode').id === episodeId;
}); });
} }

View File

@ -13,7 +13,7 @@
</div> </div>
<div class="control-group advanced-setting"> <div class="control-group advanced-setting">
<label class="control-label">Drone Factory Internal</label> <label class="control-label">Drone Factory Interval</label>
<div class="controls"> <div class="controls">
<input type="number" name="downloadedEpisodesScanInterval"/> <input type="number" name="downloadedEpisodesScanInterval"/>

View File

@ -59,13 +59,6 @@ define(
vent.trigger(vent.Events.ServerUpdated); vent.trigger(vent.Events.ServerUpdated);
} }
}); });
Messenger.show({
id : messengerId,
type : 'success',
hideAfter : 5,
message : 'Connection to backend restored'
});
}); });
this.signalRconnection.disconnected(function () { this.signalRconnection.disconnected(function () {

View File

@ -6,7 +6,7 @@ define(
], function (Backbone, StatusModel) { ], function (Backbone, StatusModel) {
return Backbone.Model.extend({ return Backbone.Model.extend({
url: function () { url: function () {
return StatusModel.get('urlBase') + '/log/' + this.get('filename'); return StatusModel.get('urlBase') + '/logfile/' + this.get('filename');
}, },
parse: function (contents) { parse: function (contents) {

View File

@ -10,7 +10,7 @@ define(
render: function () { render: function () {
this.$el.empty(); this.$el.empty();
this.$el.html('<a href="{0}/log/{1}" class="no-router" target="_blank">Download</a>'.format(StatusModel.get('urlBase'), this.cellValue)); this.$el.html('<a href="{0}/logfile/{1}" class="no-router" target="_blank">Download</a>'.format(StatusModel.get('urlBase'), this.cellValue));
return this; return this;
} }

View File

@ -6,7 +6,7 @@ define(
'System/Logs/Files/LogFileModel' 'System/Logs/Files/LogFileModel'
], function (Backbone, LogFileModel) { ], function (Backbone, LogFileModel) {
return Backbone.Collection.extend({ return Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/log/files', url : window.NzbDrone.ApiRoot + '/log/file',
model: LogFileModel, model: LogFileModel,
state: { state: {

View File

@ -67,7 +67,7 @@ define(
name : 'this', name : 'this',
label : 'Episode Title', label : 'Episode Title',
sortable : false, sortable : false,
cell : EpisodeTitleCell, cell : EpisodeTitleCell
}, },
{ {
name : 'airDateUtc', name : 'airDateUtc',
@ -121,10 +121,24 @@ define(
callback: this._searchSelected, callback: this._searchSelected,
ownerContext: this ownerContext: this
}, },
{
title: 'Search All Missing',
icon : 'icon-search',
callback: this._searchMissing,
ownerContext: this
},
{ {
title: 'Season Pass', title: 'Season Pass',
icon : 'icon-bookmark', icon : 'icon-bookmark',
route: 'seasonpass' route: 'seasonpass'
},
{
title: 'Rescan Drone Factory Folder',
icon : 'icon-refresh',
command: 'downloadedepisodesscan',
properties: {
sendUpdates: true
}
} }
] ]
}; };
@ -201,6 +215,16 @@ define(
name : 'episodeSearch', name : 'episodeSearch',
episodeIds: ids episodeIds: ids
}); });
},
_searchMissing: function () {
if (window.confirm('Are you sure you want to search for {0} missing episodes? '.format(this.collection.state.totalRecords) +
'One API request to each indexer will be used for each episode. ' +
'This cannot be stopped once started.')) {
CommandController.Execute('missingEpisodeSearch', {
name : 'missingEpisodeSearch'
});
}
} }
}); });
}); });

View File

@ -23,6 +23,8 @@
<link rel="apple-touch-icon" sizes="114x114" href="/Content/Images/touch/114.png?v=2"/> <link rel="apple-touch-icon" sizes="114x114" href="/Content/Images/touch/114.png?v=2"/>
<link rel="apple-touch-icon" sizes="144x144" href="/Content/Images/touch/144.png?v=2"/> <link rel="apple-touch-icon" sizes="144x144" href="/Content/Images/touch/144.png?v=2"/>
<link rel="icon" type="image/ico" href="/Content/Images/favicon.ico?v=2"/> <link rel="icon" type="image/ico" href="/Content/Images/favicon.ico?v=2"/>
<link rel="alternate" type="text/calendar" title="iCalendar feed for NzbDrone" href="/feed/calendar/NzbDrone.ics" />
</head> </head>
<body> <body>
<div id="nav-region"></div> <div id="nav-region"></div>

View File

@ -29,17 +29,21 @@ define(
return; return;
} }
event.preventDefault();
var href = event.target.getAttribute('href'); var href = event.target.getAttribute('href');
if (!href && $target.closest('a') && $target.closest('a')[0]) { if (!href && $target.closest('a') && $target.closest('a')[0]) {
var linkElement = $target.closest('a')[0]; var linkElement = $target.closest('a')[0];
if ($(linkElement).hasClass('no-router')) {
return;
}
href = linkElement.getAttribute('href'); href = linkElement.getAttribute('href');
} }
event.preventDefault();
if (!href) { if (!href) {
throw 'couldn\'t find route target'; throw 'couldn\'t find route target';
} }