Merge pull request #131 from Sonarr/torrents

Torrents support
pull/4/head
Taloth 10 years ago
commit ebb2b12400

@ -0,0 +1,321 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
namespace MonoTorrent.BEncoding
{
/// <summary>
/// Class representing a BEncoded Dictionary
/// </summary>
public class BEncodedDictionary : BEncodedValue, IDictionary<BEncodedString, BEncodedValue>
{
#region Member Variables
private SortedDictionary<BEncodedString, BEncodedValue> dictionary;
#endregion
#region Constructors
/// <summary>
/// Create a new BEncodedDictionary
/// </summary>
public BEncodedDictionary()
{
this.dictionary = new SortedDictionary<BEncodedString, BEncodedValue>();
}
#endregion
#region Encode/Decode Methods
/// <summary>
/// Encodes the dictionary to a byte[]
/// </summary>
/// <param name="buffer">The buffer to encode the data to</param>
/// <param name="offset">The offset to start writing the data to</param>
/// <returns></returns>
public override int Encode(byte[] buffer, int offset)
{
int written = 0;
//Dictionaries start with 'd'
buffer[offset] = (byte)'d';
written++;
foreach (KeyValuePair<BEncodedString, BEncodedValue> keypair in this)
{
written += keypair.Key.Encode(buffer, offset + written);
written += keypair.Value.Encode(buffer, offset + written);
}
// Dictionaries end with 'e'
buffer[offset + written] = (byte)'e';
written++;
return written;
}
/// <summary>
///
/// </summary>
/// <param name="reader"></param>
internal override void DecodeInternal(RawReader reader)
{
DecodeInternal(reader, reader.StrictDecoding);
}
private void DecodeInternal(RawReader reader, bool strictDecoding)
{
BEncodedString key = null;
BEncodedValue value = null;
BEncodedString oldkey = null;
if (reader.ReadByte() != 'd')
throw new BEncodingException("Invalid data found. Aborting"); // Remove the leading 'd'
while ((reader.PeekByte() != -1) && (reader.PeekByte() != 'e'))
{
key = (BEncodedString)BEncodedValue.Decode(reader); // keys have to be BEncoded strings
if (oldkey != null && oldkey.CompareTo(key) > 0)
if (strictDecoding)
throw new BEncodingException(String.Format(
"Illegal BEncodedDictionary. The attributes are not ordered correctly. Old key: {0}, New key: {1}",
oldkey, key));
oldkey = key;
value = BEncodedValue.Decode(reader); // the value is a BEncoded value
dictionary.Add(key, value);
}
if (reader.ReadByte() != 'e') // remove the trailing 'e'
throw new BEncodingException("Invalid data found. Aborting");
}
public static BEncodedDictionary DecodeTorrent(byte[] bytes)
{
return DecodeTorrent(new MemoryStream(bytes));
}
public static BEncodedDictionary DecodeTorrent(Stream s)
{
return DecodeTorrent(new RawReader(s));
}
/// <summary>
/// Special decoding method for torrent files - allows dictionary attributes to be out of order for the
/// overall torrent file, but imposes strict rules on the info dictionary.
/// </summary>
/// <returns></returns>
public static BEncodedDictionary DecodeTorrent(RawReader reader)
{
BEncodedString key = null;
BEncodedValue value = null;
BEncodedDictionary torrent = new BEncodedDictionary();
if (reader.ReadByte() != 'd')
throw new BEncodingException("Invalid data found. Aborting"); // Remove the leading 'd'
while ((reader.PeekByte() != -1) && (reader.PeekByte() != 'e'))
{
key = (BEncodedString)BEncodedValue.Decode(reader); // keys have to be BEncoded strings
if (reader.PeekByte() == 'd')
{
value = new BEncodedDictionary();
if (key.Text.ToLower().Equals("info"))
((BEncodedDictionary)value).DecodeInternal(reader, true);
else
((BEncodedDictionary)value).DecodeInternal(reader, false);
}
else
value = BEncodedValue.Decode(reader); // the value is a BEncoded value
torrent.dictionary.Add(key, value);
}
if (reader.ReadByte() != 'e') // remove the trailing 'e'
throw new BEncodingException("Invalid data found. Aborting");
return torrent;
}
#endregion
#region Helper Methods
/// <summary>
/// Returns the size of the dictionary in bytes using UTF8 encoding
/// </summary>
/// <returns></returns>
public override int LengthInBytes()
{
int length = 0;
length += 1; // Dictionaries start with 'd'
foreach (KeyValuePair<BEncodedString, BEncodedValue> keypair in this.dictionary)
{
length += keypair.Key.LengthInBytes();
length += keypair.Value.LengthInBytes();
}
length += 1; // Dictionaries end with 'e'
return length;
}
#endregion
#region Overridden Methods
public override bool Equals(object obj)
{
BEncodedValue val;
BEncodedDictionary other = obj as BEncodedDictionary;
if (other == null)
return false;
if (this.dictionary.Count != other.dictionary.Count)
return false;
foreach (KeyValuePair<BEncodedString, BEncodedValue> keypair in this.dictionary)
{
if (!other.TryGetValue(keypair.Key, out val))
return false;
if (!keypair.Value.Equals(val))
return false;
}
return true;
}
public override int GetHashCode()
{
int result = 0;
foreach (KeyValuePair<BEncodedString, BEncodedValue> keypair in dictionary)
{
result ^= keypair.Key.GetHashCode();
result ^= keypair.Value.GetHashCode();
}
return result;
}
public override string ToString()
{
return System.Text.Encoding.UTF8.GetString(Encode());
}
#endregion
#region IDictionary and IList methods
public void Add(BEncodedString key, BEncodedValue value)
{
this.dictionary.Add(key, value);
}
public void Add(KeyValuePair<BEncodedString, BEncodedValue> item)
{
this.dictionary.Add(item.Key, item.Value);
}
public void Clear()
{
this.dictionary.Clear();
}
public bool Contains(KeyValuePair<BEncodedString, BEncodedValue> item)
{
if (!this.dictionary.ContainsKey(item.Key))
return false;
return this.dictionary[item.Key].Equals(item.Value);
}
public bool ContainsKey(BEncodedString key)
{
return this.dictionary.ContainsKey(key);
}
public void CopyTo(KeyValuePair<BEncodedString, BEncodedValue>[] array, int arrayIndex)
{
this.dictionary.CopyTo(array, arrayIndex);
}
public int Count
{
get { return this.dictionary.Count; }
}
//public int IndexOf(KeyValuePair<BEncodedString, IBEncodedValue> item)
//{
// return this.dictionary.IndexOf(item);
//}
//public void Insert(int index, KeyValuePair<BEncodedString, IBEncodedValue> item)
//{
// this.dictionary.Insert(index, item);
//}
public bool IsReadOnly
{
get { return false; }
}
public bool Remove(BEncodedString key)
{
return this.dictionary.Remove(key);
}
public bool Remove(KeyValuePair<BEncodedString, BEncodedValue> item)
{
return this.dictionary.Remove(item.Key);
}
//public void RemoveAt(int index)
//{
// this.dictionary.RemoveAt(index);
//}
public bool TryGetValue(BEncodedString key, out BEncodedValue value)
{
return this.dictionary.TryGetValue(key, out value);
}
public BEncodedValue this[BEncodedString key]
{
get { return this.dictionary[key]; }
set { this.dictionary[key] = value; }
}
//public KeyValuePair<BEncodedString, IBEncodedValue> this[int index]
//{
// get { return this.dictionary[index]; }
// set { this.dictionary[index] = value; }
//}
public ICollection<BEncodedString> Keys
{
get { return this.dictionary.Keys; }
}
public ICollection<BEncodedValue> Values
{
get { return this.dictionary.Values; }
}
public IEnumerator<KeyValuePair<BEncodedString, BEncodedValue>> GetEnumerator()
{
return this.dictionary.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.dictionary.GetEnumerator();
}
#endregion
}
}

@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace MonoTorrent.BEncoding
{
/// <summary>
/// Class representing a BEncoded list
/// </summary>
public class BEncodedList : BEncodedValue, IList<BEncodedValue>
{
#region Member Variables
private List<BEncodedValue> list;
#endregion
#region Constructors
/// <summary>
/// Create a new BEncoded List with default capacity
/// </summary>
public BEncodedList()
: this(new List<BEncodedValue>())
{
}
/// <summary>
/// Create a new BEncoded List with the supplied capacity
/// </summary>
/// <param name="capacity">The initial capacity</param>
public BEncodedList(int capacity)
: this(new List<BEncodedValue>(capacity))
{
}
public BEncodedList(IEnumerable<BEncodedValue> list)
{
if (list == null)
throw new ArgumentNullException("list");
this.list = new List<BEncodedValue>(list);
}
private BEncodedList(List<BEncodedValue> value)
{
this.list = value;
}
#endregion
#region Encode/Decode Methods
/// <summary>
/// Encodes the list to a byte[]
/// </summary>
/// <param name="buffer">The buffer to encode the list to</param>
/// <param name="offset">The offset to start writing the data at</param>
/// <returns></returns>
public override int Encode(byte[] buffer, int offset)
{
int written = 0;
buffer[offset] = (byte)'l';
written++;
for (int i = 0; i < this.list.Count; i++)
written += this.list[i].Encode(buffer, offset + written);
buffer[offset + written] = (byte)'e';
written++;
return written;
}
/// <summary>
/// Decodes a BEncodedList from the given StreamReader
/// </summary>
/// <param name="reader"></param>
internal override void DecodeInternal(RawReader reader)
{
if (reader.ReadByte() != 'l') // Remove the leading 'l'
throw new BEncodingException("Invalid data found. Aborting");
while ((reader.PeekByte() != -1) && (reader.PeekByte() != 'e'))
list.Add(BEncodedValue.Decode(reader));
if (reader.ReadByte() != 'e') // Remove the trailing 'e'
throw new BEncodingException("Invalid data found. Aborting");
}
#endregion
#region Helper Methods
/// <summary>
/// Returns the size of the list in bytes
/// </summary>
/// <returns></returns>
public override int LengthInBytes()
{
int length = 0;
length += 1; // Lists start with 'l'
for (int i=0; i < this.list.Count; i++)
length += this.list[i].LengthInBytes();
length += 1; // Lists end with 'e'
return length;
}
#endregion
#region Overridden Methods
public override bool Equals(object obj)
{
BEncodedList other = obj as BEncodedList;
if (other == null)
return false;
for (int i = 0; i < this.list.Count; i++)
if (!this.list[i].Equals(other.list[i]))
return false;
return true;
}
public override int GetHashCode()
{
int result = 0;
for (int i = 0; i < list.Count; i++)
result ^= list[i].GetHashCode();
return result;
}
public override string ToString()
{
return System.Text.Encoding.UTF8.GetString(Encode());
}
#endregion
#region IList methods
public void Add(BEncodedValue item)
{
this.list.Add(item);
}
public void AddRange (IEnumerable<BEncodedValue> collection)
{
list.AddRange (collection);
}
public void Clear()
{
this.list.Clear();
}
public bool Contains(BEncodedValue item)
{
return this.list.Contains(item);
}
public void CopyTo(BEncodedValue[] array, int arrayIndex)
{
this.list.CopyTo(array, arrayIndex);
}
public int Count
{
get { return this.list.Count; }
}
public int IndexOf(BEncodedValue item)
{
return this.list.IndexOf(item);
}
public void Insert(int index, BEncodedValue item)
{
this.list.Insert(index, item);
}
public bool IsReadOnly
{
get { return false; }
}
public bool Remove(BEncodedValue item)
{
return this.list.Remove(item);
}
public void RemoveAt(int index)
{
this.list.RemoveAt(index);
}
public BEncodedValue this[int index]
{
get { return this.list[index]; }
set { this.list[index] = value; }
}
public IEnumerator<BEncodedValue> GetEnumerator()
{
return this.list.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
}
}

@ -0,0 +1,209 @@
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
namespace MonoTorrent.BEncoding
{
/// <summary>
/// Class representing a BEncoded number
/// </summary>
public class BEncodedNumber : BEncodedValue, IComparable<BEncodedNumber>
{
#region Member Variables
/// <summary>
/// The value of the BEncodedNumber
/// </summary>
public long Number
{
get { return number; }
set { number = value; }
}
internal long number;
#endregion
#region Constructors
public BEncodedNumber()
: this(0)
{
}
/// <summary>
/// Create a new BEncoded number with the given value
/// </summary>
/// <param name="initialValue">The inital value of the BEncodedNumber</param>
public BEncodedNumber(long value)
{
this.number = value;
}
public static implicit operator BEncodedNumber(long value)
{
return new BEncodedNumber(value);
}
#endregion
#region Encode/Decode Methods
/// <summary>
/// Encodes this number to the supplied byte[] starting at the supplied offset
/// </summary>
/// <param name="buffer">The buffer to write the data to</param>
/// <param name="offset">The offset to start writing the data at</param>
/// <returns></returns>
public override int Encode(byte[] buffer, int offset)
{
long number = this.number;
int written = offset;
buffer[written++] = (byte)'i';
if (number < 0)
{
buffer[written++] = (byte)'-';
number = -number;
}
// Reverse the number '12345' to get '54321'
long reversed = 0;
for (long i = number; i != 0; i /= 10)
reversed = reversed * 10 + i % 10;
// Write each digit of the reversed number to the array. We write '1'
// first, then '2', etc
for (long i = reversed; i != 0; i /= 10)
buffer[written++] = (byte)(i % 10 + '0');
if (number == 0)
buffer[written++] = (byte)'0';
// If the original number ends in one or more zeros, they are lost
// when we reverse the number. We add them back in here.
for (long i = number; i % 10 == 0 && number != 0; i /= 10)
buffer[written++] = (byte)'0';
buffer[written++] = (byte)'e';
return written - offset;
}
/// <summary>
/// Decodes a BEncoded number from the supplied RawReader
/// </summary>
/// <param name="reader">RawReader containing a BEncoded Number</param>
internal override void DecodeInternal(RawReader reader)
{
int sign = 1;
if (reader == null)
throw new ArgumentNullException("reader");
if (reader.ReadByte() != 'i') // remove the leading 'i'
throw new BEncodingException("Invalid data found. Aborting.");
if (reader.PeekByte() == '-')
{
sign = -1;
reader.ReadByte ();
}
int letter;
while (((letter = reader.PeekByte()) != -1) && letter != 'e')
{
if(letter < '0' || letter > '9')
throw new BEncodingException("Invalid number found.");
number = number * 10 + (letter - '0');
reader.ReadByte ();
}
if (reader.ReadByte() != 'e') //remove the trailing 'e'
throw new BEncodingException("Invalid data found. Aborting.");
number *= sign;
}
#endregion
#region Helper Methods
/// <summary>
/// Returns the length of the encoded string in bytes
/// </summary>
/// <returns></returns>
public override int LengthInBytes()
{
long number = this.number;
int count = 2; // account for the 'i' and 'e'
if (number == 0)
return count + 1;
if (number < 0)
{
number = -number;
count++;
}
for (long i = number; i != 0; i /= 10)
count++;
return count;
}
public int CompareTo(object other)
{
if (other is BEncodedNumber || other is long || other is int)
return CompareTo((BEncodedNumber)other);
return -1;
}
public int CompareTo(BEncodedNumber other)
{
if (other == null)
throw new ArgumentNullException("other");
return this.number.CompareTo(other.number);
}
public int CompareTo(long other)
{
return this.number.CompareTo(other);
}
#endregion
#region Overridden Methods
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
BEncodedNumber obj2 = obj as BEncodedNumber;
if (obj2 == null)
return false;
return (this.number == obj2.number);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
return this.number.GetHashCode();
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override string ToString()
{
return (this.number.ToString());
}
#endregion
}
}

@ -0,0 +1,220 @@
using System;
using System.IO;
using System.Collections;
using System.Text;
using MonoTorrent.Common;
using MonoTorrent.Messages;
namespace MonoTorrent.BEncoding
{
/// <summary>
/// Class representing a BEncoded string
/// </summary>
public class BEncodedString : BEncodedValue, IComparable<BEncodedString>
{
#region Member Variables
/// <summary>
/// The value of the BEncodedString
/// </summary>
public string Text
{
get { return Encoding.UTF8.GetString(textBytes); }
set { textBytes = Encoding.UTF8.GetBytes(value); }
}
/// <summary>
/// The underlying byte[] associated with this BEncodedString
/// </summary>
public byte[] TextBytes
{
get { return this.textBytes; }
}
private byte[] textBytes;
#endregion
#region Constructors
/// <summary>
/// Create a new BEncodedString using UTF8 encoding
/// </summary>
public BEncodedString()
: this(new byte[0])
{
}
/// <summary>
/// Create a new BEncodedString using UTF8 encoding
/// </summary>
/// <param name="value"></param>
public BEncodedString(char[] value)
: this(System.Text.Encoding.UTF8.GetBytes(value))
{
}
/// <summary>
/// Create a new BEncodedString using UTF8 encoding
/// </summary>
/// <param name="value">Initial value for the string</param>
public BEncodedString(string value)
: this(System.Text.Encoding.UTF8.GetBytes(value))
{
}
/// <summary>
/// Create a new BEncodedString using UTF8 encoding
/// </summary>
/// <param name="value"></param>
public BEncodedString(byte[] value)
{
this.textBytes = value;
}
public static implicit operator BEncodedString(string value)
{
return new BEncodedString(value);
}
public static implicit operator BEncodedString(char[] value)
{
return new BEncodedString(value);
}
public static implicit operator BEncodedString(byte[] value)
{
return new BEncodedString(value);
}
#endregion
#region Encode/Decode Methods
/// <summary>
/// Encodes the BEncodedString to a byte[] using the supplied Encoding
/// </summary>
/// <param name="buffer">The buffer to encode the string to</param>
/// <param name="offset">The offset at which to save the data to</param>
/// <param name="e">The encoding to use</param>
/// <returns>The number of bytes encoded</returns>
public override int Encode(byte[] buffer, int offset)
{
int written = offset;
written += Message.WriteAscii(buffer, written, textBytes.Length.ToString ());
written += Message.WriteAscii(buffer, written, ":");
written += Message.Write(buffer, written, textBytes);
return written - offset;
}
/// <summary>
/// Decodes a BEncodedString from the supplied StreamReader
/// </summary>
/// <param name="reader">The StreamReader containing the BEncodedString</param>
internal override void DecodeInternal(RawReader reader)
{
if (reader == null)
throw new ArgumentNullException("reader");
int letterCount;
string length = string.Empty;
while ((reader.PeekByte() != -1) && (reader.PeekByte() != ':')) // read in how many characters
length += (char)reader.ReadByte(); // the string is
if (reader.ReadByte() != ':') // remove the ':'
throw new BEncodingException("Invalid data found. Aborting");
if (!int.TryParse(length, out letterCount))
throw new BEncodingException(string.Format("Invalid BEncodedString. Length was '{0}' instead of a number", length));
this.textBytes = new byte[letterCount];
if (reader.Read(textBytes, 0, letterCount) != letterCount)
throw new BEncodingException("Couldn't decode string");
}
#endregion
#region Helper Methods
public string Hex
{
get { return BitConverter.ToString(TextBytes); }
}
public override int LengthInBytes()
{
// The length is equal to the length-prefix + ':' + length of data
int prefix = 1; // Account for ':'
// Count the number of characters needed for the length prefix
for (int i = textBytes.Length; i != 0; i = i/10)
prefix += 1;
if (textBytes.Length == 0)
prefix++;
return prefix + textBytes.Length;
}
public int CompareTo(object other)
{
return CompareTo(other as BEncodedString);
}
public int CompareTo(BEncodedString other)
{
if (other == null)
return 1;
int difference=0;
int length = this.textBytes.Length > other.textBytes.Length ? other.textBytes.Length : this.textBytes.Length;
for (int i = 0; i < length; i++)
if ((difference = this.textBytes[i].CompareTo(other.textBytes[i])) != 0)
return difference;
if (this.textBytes.Length == other.textBytes.Length)
return 0;
return this.textBytes.Length > other.textBytes.Length ? 1 : -1;
}
#endregion
#region Overridden Methods
public override bool Equals(object obj)
{
if (obj == null)
return false;
BEncodedString other;
if (obj is string)
other = new BEncodedString((string)obj);
else if (obj is BEncodedString)
other = (BEncodedString)obj;
else
return false;
return Toolbox.ByteMatch(this.textBytes, other.textBytes);
}
public override int GetHashCode()
{
int hash = 0;
for (int i = 0; i < this.textBytes.Length; i++)
hash += this.textBytes[i];
return hash;
}
public override string ToString()
{
return System.Text.Encoding.UTF8.GetString(textBytes);
}
#endregion
}
}

@ -0,0 +1,30 @@
using System;
using System.Text;
using System.Runtime.Serialization;
namespace MonoTorrent.BEncoding
{
[Serializable]
public class BEncodingException : Exception
{
public BEncodingException()
: base()
{
}
public BEncodingException(string message)
: base(message)
{
}
public BEncodingException(string message, Exception innerException)
: base(message, innerException)
{
}
protected BEncodingException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

@ -0,0 +1,203 @@
using System;
using System.IO;
using System.Text;
namespace MonoTorrent.BEncoding
{
/// <summary>
/// Base interface for all BEncoded values.
/// </summary>
public abstract class BEncodedValue
{
internal abstract void DecodeInternal(RawReader reader);
/// <summary>
/// Encodes the BEncodedValue into a byte array
/// </summary>
/// <returns>Byte array containing the BEncoded Data</returns>
public byte[] Encode()
{
byte[] buffer = new byte[LengthInBytes()];
if (Encode(buffer, 0) != buffer.Length)
throw new BEncodingException("Error encoding the data");
return buffer;
}
/// <summary>
/// Encodes the BEncodedValue into the supplied buffer
/// </summary>
/// <param name="buffer">The buffer to encode the information to</param>
/// <param name="offset">The offset in the buffer to start writing the data</param>
/// <returns></returns>
public abstract int Encode(byte[] buffer, int offset);
public static T Clone <T> (T value)
where T : BEncodedValue
{
Check.Value (value);
return (T) BEncodedValue.Decode (value.Encode ());
}
/// <summary>
/// Interface for all BEncoded values
/// </summary>
/// <param name="data">The byte array containing the BEncoded data</param>
/// <returns></returns>
public static BEncodedValue Decode(byte[] data)
{
if (data == null)
throw new ArgumentNullException("data");
using (RawReader stream = new RawReader(new MemoryStream(data)))
return (Decode(stream));
}
internal static BEncodedValue Decode(byte[] buffer, bool strictDecoding)
{
return Decode(buffer, 0, buffer.Length, strictDecoding);
}
/// <summary>
/// Decode BEncoded data in the given byte array
/// </summary>
/// <param name="buffer">The byte array containing the BEncoded data</param>
/// <param name="offset">The offset at which the data starts at</param>
/// <param name="length">The number of bytes to be decoded</param>
/// <returns>BEncodedValue containing the data that was in the byte[]</returns>
public static BEncodedValue Decode(byte[] buffer, int offset, int length)
{
return Decode(buffer, offset, length, true);
}
public static BEncodedValue Decode(byte[] buffer, int offset, int length, bool strictDecoding)
{
if (buffer == null)
throw new ArgumentNullException("buffer");
if (offset < 0 || length < 0)
throw new IndexOutOfRangeException("Neither offset or length can be less than zero");
if (offset > buffer.Length - length)
throw new ArgumentOutOfRangeException("length");
using (RawReader reader = new RawReader(new MemoryStream(buffer, offset, length), strictDecoding))
return (BEncodedValue.Decode(reader));
}
/// <summary>
/// Decode BEncoded data in the given stream
/// </summary>
/// <param name="stream">The stream containing the BEncoded data</param>
/// <returns>BEncodedValue containing the data that was in the stream</returns>
public static BEncodedValue Decode(Stream stream)
{
if (stream == null)
throw new ArgumentNullException("stream");
return Decode(new RawReader(stream));
}
/// <summary>
/// Decode BEncoded data in the given RawReader
/// </summary>
/// <param name="reader">The RawReader containing the BEncoded data</param>
/// <returns>BEncodedValue containing the data that was in the stream</returns>
public static BEncodedValue Decode(RawReader reader)
{
if (reader == null)
throw new ArgumentNullException("reader");
BEncodedValue data;
switch (reader.PeekByte())
{
case ('i'): // Integer
data = new BEncodedNumber();
break;
case ('d'): // Dictionary
data = new BEncodedDictionary();
break;
case ('l'): // List
data = new BEncodedList();
break;
case ('1'): // String
case ('2'):
case ('3'):
case ('4'):
case ('5'):
case ('6'):
case ('7'):
case ('8'):
case ('9'):
case ('0'):
data = new BEncodedString();
break;
default:
throw new BEncodingException("Could not find what value to decode");
}
data.DecodeInternal(reader);
return data;
}
/// <summary>
/// Interface for all BEncoded values
/// </summary>
/// <param name="data">The byte array containing the BEncoded data</param>
/// <returns></returns>
public static T Decode<T>(byte[] data) where T : BEncodedValue
{
return (T)BEncodedValue.Decode(data);
}
/// <summary>
/// Decode BEncoded data in the given byte array
/// </summary>
/// <param name="buffer">The byte array containing the BEncoded data</param>
/// <param name="offset">The offset at which the data starts at</param>
/// <param name="length">The number of bytes to be decoded</param>
/// <returns>BEncodedValue containing the data that was in the byte[]</returns>
public static T Decode<T>(byte[] buffer, int offset, int length) where T : BEncodedValue
{
return BEncodedValue.Decode<T>(buffer, offset, length, true);
}
public static T Decode<T>(byte[] buffer, int offset, int length, bool strictDecoding) where T : BEncodedValue
{
return (T)BEncodedValue.Decode(buffer, offset, length, strictDecoding);
}
/// <summary>
/// Decode BEncoded data in the given stream
/// </summary>
/// <param name="stream">The stream containing the BEncoded data</param>
/// <returns>BEncodedValue containing the data that was in the stream</returns>
public static T Decode<T>(Stream stream) where T : BEncodedValue
{
return (T)BEncodedValue.Decode(stream);
}
public static T Decode<T>(RawReader reader) where T : BEncodedValue
{
return (T)BEncodedValue.Decode(reader);
}
/// <summary>
/// Returns the size of the byte[] needed to encode this BEncodedValue
/// </summary>
/// <returns></returns>
public abstract int LengthInBytes();
}
}

@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace MonoTorrent.BEncoding
{
public class RawReader : Stream
{
bool hasPeek;
Stream input;
byte[] peeked;
bool strictDecoding;
public bool StrictDecoding
{
get { return strictDecoding; }
}
public RawReader(Stream input)
: this(input, true)
{
}
public RawReader(Stream input, bool strictDecoding)
{
this.input = input;
this.peeked = new byte[1];
this.strictDecoding = strictDecoding;
}
public override bool CanRead
{
get { return input.CanRead; }
}
public override bool CanSeek
{
get { return input.CanSeek; }
}
public override bool CanWrite
{
get { return false; }
}
public override void Flush()
{
throw new NotSupportedException();
}
public override long Length
{
get { return input.Length; }
}
public int PeekByte()
{
if (!hasPeek)
hasPeek = Read(peeked, 0, 1) == 1;
return hasPeek ? peeked[0] : -1;
}
public override int ReadByte()
{
if (hasPeek)
{
hasPeek = false;
return peeked[0];
}
return base.ReadByte();
}
public override long Position
{
get
{
if (hasPeek)
return input.Position - 1;
return input.Position;
}
set
{
if (value != Position)
{
hasPeek = false;
input.Position = value;
}
}
}
public override int Read(byte[] buffer, int offset, int count)
{
int read = 0;
if (hasPeek && count > 0)
{
hasPeek = false;
buffer[offset] = peeked[0];
offset++;
count--;
read++;
}
read += input.Read(buffer, offset, count);
return read;
}
public override long Seek(long offset, SeekOrigin origin)
{
long val;
if (hasPeek && origin == SeekOrigin.Current)
val = input.Seek(offset - 1, origin);
else
val = input.Seek(offset, origin);
hasPeek = false;
return val;
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
}
}

@ -0,0 +1,420 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace MonoTorrent
{
/// <summary>
/// This class is for represting the Peer's bitfield
/// </summary>
public class BitField : ICloneable, IEnumerable<bool>
{
#region Member Variables
private int[] array;
private int length;
private int trueCount;
internal bool AllFalse
{
get { return this.trueCount == 0; }
}
internal bool AllTrue
{
get { return this.trueCount == this.length; }
}
public int Length
{
get { return this.length; }
}
public double PercentComplete
{
get { return (double)this.trueCount / this.length * 100.0; }
}
#endregion
#region Constructors
public BitField(byte[] array, int length)
: this(length)
{
this.FromArray(array, 0, array.Length);
}
public BitField(int length)
{
if (length < 0)
throw new ArgumentOutOfRangeException("length");
this.length = length;
this.array = new int[(length + 31) / 32];
}
public BitField(bool[] array)
{
this.length = array.Length;
this.array = new int[(array.Length + 31) / 32];
for (int i = 0; i < array.Length; i++)
this.Set(i, array[i]);
}
#endregion
#region Methods BitArray
public bool this[int index]
{
get { return this.Get(index); }
internal set { this.Set(index, value); }
}
object ICloneable.Clone()
{
return this.Clone();
}
public BitField Clone()
{
BitField b = new BitField(this.length);
Buffer.BlockCopy(this.array, 0, b.array, 0, this.array.Length * 4);
b.trueCount = this.trueCount;
return b;
}
public BitField From(BitField value)
{
this.Check(value);
Buffer.BlockCopy(value.array, 0, this.array, 0, this.array.Length * 4);
this.trueCount = value.trueCount;
return this;
}
public BitField Not()
{
for (int i = 0; i < this.array.Length; i++)
this.array[i] = ~this.array[i];
this.trueCount = this.length - this.trueCount;
return this;
}
public BitField And(BitField value)
{
this.Check(value);
for (int i = 0; i < this.array.Length; i++)
this.array[i] &= value.array[i];
this.Validate();
return this;
}
internal BitField NAnd(BitField value)
{
this.Check(value);
for (int i = 0; i < this.array.Length; i++)
this.array[i] &= ~value.array[i];
this.Validate();
return this;
}
public BitField Or(BitField value)
{
this.Check(value);
for (int i = 0; i < this.array.Length; i++)
this.array[i] |= value.array[i];
this.Validate();
return this;
}
public BitField Xor(BitField value)
{
this.Check(value);
for (int i = 0; i < this.array.Length; i++)
this.array[i] ^= value.array[i];
this.Validate();
return this;
}
public override bool Equals(object obj)
{
BitField bf = obj as BitField;
if (bf == null || this.array.Length != bf.array.Length || this.TrueCount != bf.TrueCount)
return false;
for (int i = 0; i < this.array.Length; i++)
if (this.array[i] != bf.array[i])
return false;
return true;
}
public int FirstTrue()
{
return this.FirstTrue(0, this.length);
}
public int FirstTrue(int startIndex, int endIndex)
{
int start;
int end;
// If the number of pieces is an exact multiple of 32, we need to decrement by 1 so we don't overrun the array
// For the case when endIndex == 0, we need to ensure we don't go negative
int loopEnd = Math.Min((endIndex / 32), this.array.Length - 1);
for (int i = (startIndex / 32); i <= loopEnd; i++)
{
if (this.array[i] == 0) // This one has no true values
continue;
start = i * 32;
end = start + 32;
start = (start < startIndex) ? startIndex : start;
end = (end > this.length) ? this.length : end;
end = (end > endIndex) ? endIndex : end;
if (end == this.Length && end > 0)
end--;
for (int j = start; j <= end; j++)
if (this.Get(j)) // This piece is true
return j;
}
return -1; // Nothing is true
}
public int FirstFalse()
{
return this.FirstFalse(0, this.Length);
}
public int FirstFalse(int startIndex, int endIndex)
{
int start;
int end;
// If the number of pieces is an exact multiple of 32, we need to decrement by 1 so we don't overrun the array
// For the case when endIndex == 0, we need to ensure we don't go negative
int loopEnd = Math.Min((endIndex / 32), this.array.Length - 1);
for (int i = (startIndex / 32); i <= loopEnd; i++)
{
if (this.array[i] == ~0) // This one has no false values
continue;
start = i * 32;
end = start + 32;
start = (start < startIndex) ? startIndex : start;
end = (end > this.length) ? this.length : end;
end = (end > endIndex) ? endIndex : end;
if (end == this.Length && end > 0)
end--;
for (int j = start; j <= end; j++)
if (!this.Get(j)) // This piece is true
return j;
}
return -1; // Nothing is true
}
internal void FromArray(byte[] buffer, int offset, int length)
{
int end = this.Length / 32;
for (int i = 0; i < end; i++)
this.array[i] = (buffer[offset++] << 24) |
(buffer[offset++] << 16) |
(buffer[offset++] << 8) |
(buffer[offset++] << 0);
int shift = 24;
for (int i = end * 32; i < this.Length; i += 8)
{
this.array[this.array.Length - 1] |= buffer[offset++] << shift;
shift -= 8;
}
this.Validate();
}
bool Get(int index)
{
if (index < 0 || index >= this.length)
throw new ArgumentOutOfRangeException("index");
return (this.array[index >> 5] & (1 << (31 - (index & 31)))) != 0;
}
public IEnumerator<bool> GetEnumerator()
{
for (int i = 0; i < this.length; i++)
yield return this.Get(i);
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
public override int GetHashCode()
{
int count = 0;
for (int i = 0; i < this.array.Length; i++)
count += this.array[i];
return count;
}
public int LengthInBytes
{
get { return (this.length + 7) / 8; } //8 bits in a byte.
}
public BitField Set(int index, bool value)
{
if (index < 0 || index >= this.length)
throw new ArgumentOutOfRangeException("index");
if (value)
{
if ((this.array[index >> 5] & (1 << (31 - (index & 31)))) == 0)// If it's not already true
this.trueCount++; // Increase true count
this.array[index >> 5] |= (1 << (31 - index & 31));
}
else
{
if ((this.array[index >> 5] & (1 << (31 - (index & 31)))) != 0)// If it's not already false
this.trueCount--; // Decrease true count
this.array[index >> 5] &= ~(1 << (31 - (index & 31)));
}
return this;
}
internal BitField SetTrue(params int[] indices)
{
foreach (int index in indices)
this.Set(index, true);
return this;
}
internal BitField SetFalse(params int[] indices)
{
foreach (int index in indices)
this.Set(index, false);
return this;
}
internal BitField SetAll(bool value)
{
if (value)
{
for (int i = 0; i < this.array.Length; i++)
this.array[i] = ~0;
this.Validate();
}
else
{
for (int i = 0; i < this.array.Length; i++)
this.array[i] = 0;
this.trueCount = 0;
}
return this;
}
internal byte[] ToByteArray()
{
byte[] data = new byte[this.LengthInBytes];
this.ToByteArray(data, 0);
return data;
}
internal void ToByteArray(byte[] buffer, int offset)
{
if (buffer == null)
throw new ArgumentNullException("buffer");
this.ZeroUnusedBits();
int end = this.Length / 32;
for (int i = 0; i < end; i++)
{
buffer[offset++] = (byte)(this.array[i] >> 24);
buffer[offset++] = (byte)(this.array[i] >> 16);
buffer[offset++] = (byte)(this.array[i] >> 8);
buffer[offset++] = (byte)(this.array[i] >> 0);
}
int shift = 24;
for (int i = end * 32; i < this.Length; i += 8)
{
buffer[offset++] = (byte)(this.array[this.array.Length - 1] >> shift);
shift -= 8;
}
}
public override string ToString()
{
StringBuilder sb = new StringBuilder(this.array.Length * 16);
for (int i = 0; i < this.Length; i++)
{
sb.Append(this.Get(i) ? 'T' : 'F');
sb.Append(' ');
}
return sb.ToString(0, sb.Length - 1);
}
public int TrueCount
{
get { return this.trueCount; }
}
void Validate()
{
this.ZeroUnusedBits();
// Update the population count
uint count = 0;
for (int i = 0; i < this.array.Length; i++)
{
uint v = (uint)this.array[i];
v = v - ((v >> 1) & 0x55555555);
v = (v & 0x33333333) + ((v >> 2) & 0x33333333);
count += (((v + (v >> 4) & 0xF0F0F0F) * 0x1010101)) >> 24;
}
this.trueCount = (int)count ;
}
void ZeroUnusedBits()
{
if (this.array.Length == 0)
return;
// Zero the unused bits
int shift = 32 - this.length % 32;
if (shift != 0)
this.array[this.array.Length - 1] &= (-1 << shift);
}
void Check(BitField value)
{
MonoTorrent.Check.Value(value);
if (this.length != value.length)
throw new ArgumentException("BitFields are of different lengths", "value");
}
#endregion
}
}

@ -0,0 +1,235 @@
using System;
namespace MonoTorrent
{
public static class Check
{
static void DoCheck(object toCheck, string name)
{
if (toCheck == null)
throw new ArgumentNullException(name);
}
static void IsNullOrEmpty(string toCheck, string name)
{
DoCheck(toCheck, name);
if (toCheck.Length == 0)
throw new ArgumentException("Cannot be empty", name);
}
public static void Address(object address)
{
DoCheck(address, "address");
}
public static void AddressRange(object addressRange)
{
DoCheck(addressRange, "addressRange");
}
public static void AddressRanges(object addressRanges)
{
DoCheck(addressRanges, "addressRanges");
}
public static void Announces(object announces)
{
DoCheck(announces, "announces");
}
public static void BaseDirectory(object baseDirectory)
{
DoCheck(baseDirectory, "baseDirectory");
}
internal static void BaseType(Type baseType)
{
DoCheck(baseType, "baseType");
}
internal static void Buffer(object buffer)
{
DoCheck(buffer, "buffer");
}
internal static void Cache(object cache)
{
DoCheck(cache, "cache");
}
public static void Data(object data)
{
DoCheck(data, "data");
}
public static void Destination (object destination)
{
DoCheck (destination, "destination");
}
public static void Endpoint(object endpoint)
{
DoCheck(endpoint, "endpoint");
}
public static void File(object file)
{
DoCheck(file, "file");
}
public static void Files(object files)
{
DoCheck(files, "files");
}
public static void FileSource(object fileSource)
{
DoCheck(fileSource, "fileSource");
}
public static void InfoHash(object infoHash)
{
DoCheck(infoHash, "infoHash");
}
public static void Key (object key)
{
DoCheck (key, "key");
}
public static void Limiter(object limiter)
{
DoCheck(limiter, "limiter");
}
public static void Listener(object listener)
{
DoCheck(listener, "listener");
}
public static void Location(object location)
{
DoCheck(location, "location");
}
public static void MagnetLink(object magnetLink)
{
DoCheck(magnetLink, "magnetLink");
}
public static void Manager(object manager)
{
DoCheck(manager, "manager");
}
public static void Mappings (object mappings)
{
DoCheck (mappings, "mappings");
}
public static void Metadata(object metadata)
{
DoCheck(metadata, "metadata");
}
public static void Name (object name)
{
DoCheck (name, "name");
}
public static void Path(object path)
{
DoCheck(path, "path");
}
public static void Paths (object paths)
{
DoCheck (paths, "paths");
}
public static void PathNotEmpty(string path)
{
IsNullOrEmpty(path, "path");
}
public static void Peer (object peer)
{
DoCheck (peer, "peer");
}
public static void Peers (object peers)
{
DoCheck (peers, "peers");
}
public static void Picker(object picker)
{
DoCheck(picker, "picker");
}
public static void Result(object result)
{
DoCheck(result, "result");
}
public static void SavePath(object savePath)
{
DoCheck(savePath, "savePath");
}
public static void Settings(object settings)
{
DoCheck(settings, "settings");
}
internal static void SpecificType(Type specificType)
{
DoCheck(specificType, "specificType");
}
public static void Stream(object stream)
{
DoCheck(stream, "stream");
}
public static void Torrent(object torrent)
{
DoCheck(torrent, "torrent");
}
public static void TorrentInformation(object torrentInformation)
{
DoCheck(torrentInformation, "torrentInformation");
}
public static void TorrentSave(object torrentSave)
{
DoCheck(torrentSave, "torrentSave");
}
public static void Tracker(object tracker)
{
DoCheck(tracker, "tracker");
}
public static void Url(object url)
{
DoCheck(url, "url");
}
public static void Uri(Uri uri)
{
DoCheck(uri, "uri");
}
public static void Value(object value)
{
DoCheck(value, "value");
}
public static void Writer(object writer)
{
DoCheck(writer, "writer");
}
}
}

@ -0,0 +1,13 @@
namespace MonoTorrent
{
public enum Priority
{
DoNotDownload = 0,
Lowest = 1,
Low = 2,
Normal = 4,
High = 8,
Highest = 16,
Immediate = 32
}
}

@ -0,0 +1,30 @@
using System;
namespace MonoTorrent.Exceptions
{
public class MessageException : TorrentException
{
public MessageException()
: base()
{
}
public MessageException(string message)
: base(message)
{
}
public MessageException(string message, Exception innerException)
: base(message, innerException)
{
}
public MessageException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
: base(info, context)
{
}
}
}

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace MonoTorrent
{
public static class HashAlgoFactory
{
static Dictionary<Type, Type> algos = new Dictionary<Type, Type>();
static HashAlgoFactory()
{
Register<MD5, MD5CryptoServiceProvider>();
Register<SHA1, SHA1CryptoServiceProvider>();
}
public static void Register<T, U>()
where T : HashAlgorithm
where U : HashAlgorithm
{
Register(typeof(T), typeof(U));
}
public static void Register(Type baseType, Type specificType)
{
Check.BaseType(baseType);
Check.SpecificType(specificType);
lock (algos)
algos[baseType] = specificType;
}
public static T Create<T>()
where T : HashAlgorithm
{
if (algos.ContainsKey(typeof(T)))
return (T)Activator.CreateInstance(algos[typeof(T)]);
return null;
}
}
}

@ -0,0 +1,93 @@
using System;
namespace MonoTorrent
{
public class Hashes
{
#region Constants
/// <summary>
/// Hash code length (in bytes)
/// </summary>
internal static readonly int HashCodeLength = 20;
#endregion
#region Private Fields
private int count;
private byte[] hashData;
#endregion Private Fields
#region Properties
/// <summary>
/// Number of Hashes (equivalent to number of Pieces)
/// </summary>
public int Count
{
get { return this.count; }
}
#endregion Properties
#region Constructors
internal Hashes(byte[] hashData, int count)
{
this.hashData = hashData;
this.count = count;
}
#endregion Constructors
#region Methods
/// <summary>
/// Determine whether a calculated hash is equal to our stored hash
/// </summary>
/// <param name="hash">Hash code to check</param>
/// <param name="hashIndex">Index of hash/piece to verify against</param>
/// <returns>true iff hash is equal to our stored hash, false otherwise</returns>
public bool IsValid(byte[] hash, int hashIndex)
{
if (hash == null)
throw new ArgumentNullException("hash");
if (hash.Length != HashCodeLength)
throw new ArgumentException(string.Format("Hash must be {0} bytes in length", HashCodeLength), "hash");
if (hashIndex < 0 || hashIndex > this.count)
throw new ArgumentOutOfRangeException("hashIndex", string.Format("hashIndex must be between 0 and {0}", this.count));
int start = hashIndex * HashCodeLength;
for (int i = 0; i < HashCodeLength; i++)
if (hash[i] != this.hashData[i + start])
return false;
return true;
}
/// <summary>
/// Returns the hash for a specific piece
/// </summary>
/// <param name="hashIndex">Piece/hash index to return</param>
/// <returns>byte[] (length HashCodeLength) containing hashdata</returns>
public byte[] ReadHash(int hashIndex)
{
if (hashIndex < 0 || hashIndex >= this.count)
throw new ArgumentOutOfRangeException("hashIndex");
// Read out our specified piece's hash data
byte[] hash = new byte[HashCodeLength];
Buffer.BlockCopy(this.hashData, hashIndex * HashCodeLength, hash, 0, HashCodeLength);
return hash;
}
#endregion Methods
}
}

@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Text;
using MonoTorrent.Common;
using System.Web;
namespace MonoTorrent
{
public class InfoHash : IEquatable <InfoHash>
{
static Dictionary<char, byte> base32DecodeTable;
static InfoHash()
{
base32DecodeTable = new Dictionary<char, byte>();
string table = "abcdefghijklmnopqrstuvwxyz234567";
for (int i = 0; i < table.Length; i++)
base32DecodeTable[table[i]] = (byte)i;
}
byte[] hash;
internal byte[] Hash
{
get { return hash; }
}
public InfoHash(byte[] infoHash)
{
Check.InfoHash(infoHash);
if (infoHash.Length != 20)
throw new ArgumentException("Infohash must be exactly 20 bytes long");
hash = (byte[])infoHash.Clone();
}
public override bool Equals(object obj)
{
return Equals(obj as InfoHash);
}
public bool Equals(byte[] other)
{
return other == null || other.Length != 20 ? false : Toolbox.ByteMatch(Hash, other);
}
public bool Equals(InfoHash other)
{
return this == other;
}
public override int GetHashCode()
{
// Equality is based generally on checking 20 positions, checking 4 should be enough
// for the hashcode as infohashes are randomly distributed.
return Hash[0] | (Hash[1] << 8) | (Hash[2] << 16) | (Hash[3] << 24);
}
public byte[] ToArray()
{
return (byte[])hash.Clone();
}
public string ToHex()
{
StringBuilder sb = new StringBuilder(40);
for (int i = 0; i < hash.Length; i++)
{
string hex = hash[i].ToString("X");
if (hex.Length != 2)
sb.Append("0");
sb.Append(hex);
}
return sb.ToString();
}
public override string ToString()
{
return BitConverter.ToString(hash);
}
public string UrlEncode()
{
return UriHelper.UrlEncode(Hash);
}
public static bool operator ==(InfoHash left, InfoHash right)
{
if ((object)left == null)
return (object)right == null;
if ((object)right == null)
return false;
return Toolbox.ByteMatch(left.Hash, right.Hash);
}
public static bool operator !=(InfoHash left, InfoHash right)
{
return !(left == right);
}
public static InfoHash FromBase32(string infoHash)
{
Check.InfoHash (infoHash);
if (infoHash.Length != 32)
throw new ArgumentException("Infohash must be a base32 encoded 32 character string");
infoHash = infoHash.ToLower();
int infohashOffset =0 ;
byte[] hash = new byte[20];
var temp = new byte[8];
for (int i = 0; i < hash.Length; ) {
for (int j=0; j < 8; j++)
if (!base32DecodeTable.TryGetValue(infoHash[infohashOffset++], out temp[j]))
throw new ArgumentException ("infoHash", "Value is not a valid base32 encoded string");
//8 * 5bits = 40 bits = 5 bytes
hash[i++] = (byte)((temp[0] << 3) | (temp [1]>> 2));
hash[i++] = (byte)((temp[1] << 6) | (temp[2] << 1) | (temp[3] >> 4));
hash[i++] = (byte)((temp[3] << 4) | (temp [4]>> 1));
hash[i++] = (byte)((temp[4] << 7) | (temp[5] << 2) | (temp [6]>> 3));
hash[i++] = (byte)((temp[6] << 5) | temp[7]);
}
return new InfoHash(hash);
}
public static InfoHash FromHex(string infoHash)
{
Check.InfoHash (infoHash);
if (infoHash.Length != 40)
throw new ArgumentException("Infohash must be 40 characters long");
byte[] hash = new byte[20];
for (int i = 0; i < hash.Length; i++)
hash[i] = byte.Parse(infoHash.Substring(i * 2, 2), System.Globalization.NumberStyles.HexNumber);
return new InfoHash(hash);
}
public static InfoHash FromMagnetLink(string magnetLink)
{
Check.MagnetLink(magnetLink);
if (!magnetLink.StartsWith("magnet:?"))
throw new ArgumentException("Invalid magnet link format");
magnetLink = magnetLink.Substring("magnet:?".Length);
int hashStart = magnetLink.IndexOf("xt=urn:btih:");
if (hashStart == -1)
throw new ArgumentException("Magnet link does not contain an infohash");
hashStart += "xt=urn:btih:".Length;
int hashEnd = magnetLink.IndexOf('&', hashStart);
if (hashEnd == -1)
hashEnd = magnetLink.Length;
switch (hashEnd - hashStart)
{
case 32:
return FromBase32(magnetLink.Substring(hashStart, 32));
case 40:
return FromHex(magnetLink.Substring(hashStart, 40));
default:
throw new ArgumentException("Infohash must be base32 or hex encoded.");
}
}
public static InfoHash UrlDecode(string infoHash)
{
Check.InfoHash(infoHash);
return new InfoHash(UriHelper.UrlDecode(infoHash));
}
}
}

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MonoTorrent
{
public class MagnetLink
{
public RawTrackerTier AnnounceUrls {
get; private set;
}
public InfoHash InfoHash {
get; private set;
}
public string Name {
get; private set;
}
public List<string> Webseeds {
get; private set;
}
public MagnetLink (string url)
{
Check.Url (url);
AnnounceUrls = new RawTrackerTier ();
Webseeds = new List<string> ();
ParseMagnetLink (url);
}
void ParseMagnetLink (string url)
{
string[] splitStr = url.Split ('?');
if (splitStr.Length == 0 || splitStr[0] != "magnet:")
throw new FormatException ("The magnet link must start with 'magnet:?'.");
if (splitStr.Length == 1)
return;//no parametter
string[] parameters = splitStr[1].Split ('&', ';');
for (int i = 0; i < parameters.Length ; i++)
{
string[] keyval = parameters[i].Split ('=');
if (keyval.Length != 2)
throw new FormatException ("A field-value pair of the magnet link contain more than one equal'.");
switch (keyval[0].Substring(0, 2))
{
case "xt"://exact topic
if (InfoHash != null)
throw new FormatException ("More than one infohash in magnet link is not allowed.");
string val = keyval[1].Substring(9);
switch (keyval[1].Substring(0, 9))
{
case "urn:sha1:"://base32 hash
case "urn:btih:":
if (val.Length == 32)
InfoHash = InfoHash.FromBase32 (val);
else if (val.Length == 40)
InfoHash = InfoHash.FromHex (val);
else
throw new FormatException("Infohash must be base32 or hex encoded.");
break;
}
break;
case "tr" ://address tracker
var bytes = UriHelper.UrlDecode(keyval[1]);
AnnounceUrls.Add(Encoding.UTF8.GetString(bytes));
break;
case "as"://Acceptable Source
Webseeds.Add (keyval[1]);
break;
case "dn"://display name
var name = UriHelper.UrlDecode(keyval[1]);
Name = Encoding.UTF8.GetString(name);
break;
case "xl"://exact length
case "xs":// eXact Source - P2P link.
case "kt"://keyword topic
case "mt"://manifest topic
//not supported for moment
break;
default:
//not supported
break;
}
}
}
}
}

@ -0,0 +1,12 @@
namespace MonoTorrent.Messages
{
interface IMessage
{
int ByteLength { get;}
byte[] Encode();
int Encode(byte[] buffer, int offset);
void Decode(byte[] buffer, int offset, int length);
}
}

@ -0,0 +1,164 @@
using System;
using System.Net;
using MonoTorrent.Exceptions;
namespace MonoTorrent.Messages
{
public abstract class Message : IMessage
{
public abstract int ByteLength { get; }
protected int CheckWritten(int written)
{
if (written != this.ByteLength)
throw new MessageException("Message encoded incorrectly. Incorrect number of bytes written");
return written;
}
public abstract void Decode(byte[] buffer, int offset, int length);
public byte[] Encode()
{
byte[] buffer = new byte[this.ByteLength];
this.Encode(buffer, 0);
return buffer;
}
public abstract int Encode(byte[] buffer, int offset);
static public byte ReadByte(byte[] buffer, int offset)
{
return buffer[offset];
}
static public byte ReadByte(byte[] buffer, ref int offset)
{
byte b = buffer[offset];
offset++;
return b;
}
static public byte[] ReadBytes(byte[] buffer, int offset, int count)
{
return ReadBytes(buffer, ref offset, count);
}
static public byte[] ReadBytes(byte[] buffer, ref int offset, int count)
{
byte[] result = new byte[count];
Buffer.BlockCopy(buffer, offset, result, 0, count);
offset += count;
return result;
}
static public short ReadShort(byte[] buffer, int offset)
{
return ReadShort(buffer, ref offset);
}
static public short ReadShort(byte[] buffer, ref int offset)
{
short ret = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(buffer, offset));
offset += 2;
return ret;
}
static public string ReadString(byte[] buffer, int offset, int count)
{
return ReadString(buffer, ref offset, count);
}
static public string ReadString(byte[] buffer, ref int offset, int count)
{
string s = System.Text.Encoding.ASCII.GetString(buffer, offset, count);
offset += count;
return s;
}
static public int ReadInt(byte[] buffer, int offset)
{
return ReadInt(buffer, ref offset);
}
static public int ReadInt(byte[] buffer, ref int offset)
{
int ret = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buffer, offset));
offset += 4;
return ret;
}
static public long ReadLong(byte[] buffer, int offset)
{
return ReadLong(buffer, ref offset);
}
static public long ReadLong(byte[] buffer, ref int offset)
{
long ret = IPAddress.NetworkToHostOrder(BitConverter.ToInt64(buffer, offset));
offset += 8;
return ret;
}
static public int Write(byte[] buffer, int offset, byte value)
{
buffer[offset] = value;
return 1;
}
static public int Write(byte[] dest, int destOffset, byte[] src, int srcOffset, int count)
{
Buffer.BlockCopy(src, srcOffset, dest, destOffset, count);
return count;
}
static public int Write(byte[] buffer, int offset, ushort value)
{
return Write(buffer, offset, (short)value);
}
static public int Write(byte[] buffer, int offset, short value)
{
offset += Write(buffer, offset, (byte)(value >> 8));
offset += Write(buffer, offset, (byte)value);
return 2;
}
static public int Write(byte[] buffer, int offset, int value)
{
offset += Write(buffer, offset, (byte)(value >> 24));
offset += Write(buffer, offset, (byte)(value >> 16));
offset += Write(buffer, offset, (byte)(value >> 8));
offset += Write(buffer, offset, (byte)(value));
return 4;
}
static public int Write(byte[] buffer, int offset, uint value)
{
return Write(buffer, offset, (int)value);
}
static public int Write(byte[] buffer, int offset, long value)
{
offset += Write(buffer, offset, (int)(value >> 32));
offset += Write(buffer, offset, (int)value);
return 8;
}
static public int Write(byte[] buffer, int offset, ulong value)
{
return Write(buffer, offset, (long)value);
}
static public int Write(byte[] buffer, int offset, byte[] value)
{
return Write(buffer, offset, value, 0, value.Length);
}
static public int WriteAscii(byte[] buffer, int offset, string text)
{
for (int i = 0; i < text.Length; i++)
Write(buffer, offset + i, (byte)text[i]);
return text.Length;
}
}
}

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProjectType>Local</ProjectType>
<ProductVersion>9.0.21022</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>MonoTorrent</RootNamespace>
<AssemblyName>MonoTorrent</AssemblyName>
<AssemblyKeyContainerName>
</AssemblyKeyContainerName>
<DefaultClientScript>JScript</DefaultClientScript>
<DefaultHTMLPageLayout>Grid</DefaultHTMLPageLayout>
<DefaultTargetSchema>IE50</DefaultTargetSchema>
<DelaySign>false</DelaySign>
<AppDesignerFolder>
</AppDesignerFolder>
<RootNamespace>MonoTorrent</RootNamespace>
<FileUpgradeFlags>
</FileUpgradeFlags>
<OldToolsVersion>3.5</OldToolsVersion>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<TargetFrameworkProfile />
<FileAlignment>512</FileAlignment>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\_output\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\_output\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="BEncoding\RawReader.cs" />
<Compile Include="BEncoding\BEncodedDictionary.cs" />
<Compile Include="BEncoding\BEncodedList.cs" />
<Compile Include="BEncoding\BEncodedNumber.cs" />
<Compile Include="BEncoding\BEncodedString.cs" />
<Compile Include="BEncoding\BEncodingException.cs" />
<Compile Include="BEncoding\IBEncodedValue.cs" />
<Compile Include="Exceptions\MessageException.cs" />
<Compile Include="Messages\IMessage.cs" />
<Compile Include="Messages\Message.cs" />
<Compile Include="BitField.cs" />
<Compile Include="Check.cs" />
<Compile Include="Enums.cs" />
<Compile Include="HashAlgoFactory.cs" />
<Compile Include="Hashes.cs" />
<Compile Include="InfoHash.cs" />
<Compile Include="ToolBox.cs" />
<Compile Include="Torrent.cs" />
<Compile Include="TorrentException.cs" />
<Compile Include="TorrentFile.cs" />
<Compile Include="MagnetLink.cs" />
<Compile Include="UriHelper.cs" />
<Compile Include="RawTrackerTiers.cs" />
<Compile Include="RawTrackerTier.cs" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Framework.2.0">
<Visible>False</Visible>
<ProductName>.NET Framework 2.0 %28x86%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.0">
<Visible>False</Visible>
<ProductName>.NET Framework 3.0 %28x86%29</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
</Project>

@ -0,0 +1,97 @@
using System;
using System.Collections;
using System.Collections.Generic;
using MonoTorrent.BEncoding;
namespace MonoTorrent
{
public class RawTrackerTier : IList<string>
{
public string this[int index] {
get { return ((BEncodedString) Tier [index]).Text; }
set { Tier [index] = new BEncodedString (value );}
}
internal BEncodedList Tier {
get; set;
}
public RawTrackerTier ()
: this (new BEncodedList ())
{
}
public RawTrackerTier (BEncodedList tier)
{
Tier = tier;
}
public RawTrackerTier (IEnumerable<string> announces)
: this ()
{
foreach (var v in announces)
Add (v);
}
public int IndexOf (string item)
{
return Tier.IndexOf ((BEncodedString) item);
}
public void Insert (int index, string item)
{
Tier.Insert (index, (BEncodedString) item);
}
public void RemoveAt (int index)
{
Tier.RemoveAt (index);
}
public void Add (string item)
{
Tier.Add ((BEncodedString) item);
}
public void Clear ()
{
Tier.Clear ();
}
public bool Contains (string item)
{
return Tier.Contains ((BEncodedString) item);
}
public void CopyTo (string[] array, int arrayIndex)
{
foreach (var s in this)
array [arrayIndex ++] = s;
}
public bool Remove (string item)
{
return Tier.Remove ((BEncodedString) item);
}
public int Count {
get { return Tier.Count; }
}
public bool IsReadOnly {
get { return Tier.IsReadOnly; }
}
public IEnumerator<string> GetEnumerator ()
{
foreach (BEncodedString v in Tier)
yield return v.Text;
}
IEnumerator IEnumerable.GetEnumerator ()
{
return GetEnumerator ();
}
}
}

@ -0,0 +1,105 @@
using System;
using System.Collections;
using System.Collections.Generic;
using MonoTorrent.BEncoding;
namespace MonoTorrent
{
public class RawTrackerTiers : IList<RawTrackerTier>
{
BEncodedList Tiers {
get; set;
}
public RawTrackerTiers ()
: this (new BEncodedList ())
{
}
public RawTrackerTiers (BEncodedList tiers)
{
Tiers = tiers;
}
public int IndexOf (RawTrackerTier item)
{
if (item != null) {
for (int i = 0; i < Tiers.Count; i++)
if (item.Tier == Tiers [i])
return i;
}
return -1;
}
public void Insert (int index, RawTrackerTier item)
{
Tiers.Insert (index, item.Tier);
}
public void RemoveAt (int index)
{
Tiers.RemoveAt (index);
}
public RawTrackerTier this[int index] {
get { return new RawTrackerTier ((BEncodedList) Tiers [index]); }
set { Tiers [index] = value.Tier; }
}
public void Add (RawTrackerTier item)
{
Tiers.Add (item.Tier);
}
public void AddRange (IEnumerable<RawTrackerTier> tiers)
{
foreach (var v in tiers)
Add (v);
}
public void Clear ()
{
Tiers.Clear ();
}
public bool Contains (RawTrackerTier item)
{
return IndexOf (item) != -1;
}
public void CopyTo (RawTrackerTier[] array, int arrayIndex)
{
foreach (var v in this)
array [arrayIndex ++] = v;
}
public bool Remove (RawTrackerTier item)
{
int index = IndexOf (item);
if (index != -1)
RemoveAt (index);
return index != -1;
}
public int Count {
get { return Tiers.Count; }
}
public bool IsReadOnly {
get { return Tiers.IsReadOnly; }
}
public IEnumerator<RawTrackerTier> GetEnumerator ()
{
foreach (var v in Tiers)
yield return new RawTrackerTier ((BEncodedList) v);
}
IEnumerator IEnumerable.GetEnumerator ()
{
return GetEnumerator ();
}
}
}

@ -0,0 +1,125 @@
using System;
using System.Collections;
using System.Text;
using System.Collections.Generic;
using System.Threading;
namespace MonoTorrent.Common
{
public delegate long Operation<T>(T target);
public static class Toolbox
{
private static Random r = new Random();
public static int Count<T>(IEnumerable<T> enumerable, Predicate<T> predicate)
{
int count = 0;
foreach (T t in enumerable)
if (predicate(t))
count++;
return count;
}
public static long Accumulate<T>(IEnumerable<T> enumerable, Operation<T> action)
{
long count = 0;
foreach (T t in enumerable)
count += action(t);
return count;
}
public static void RaiseAsyncEvent<T>(EventHandler<T> e, object o, T args)
where T : EventArgs
{
if (e == null)
return;
ThreadPool.QueueUserWorkItem(delegate {
if (e != null)
e(o, args);
});
}
/// <summary>
/// Randomizes the contents of the array
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="array"></param>
public static void Randomize<T>(List<T> array)
{
List<T> clone = new List<T>(array);
array.Clear();
while (clone.Count > 0)
{
int index = r.Next(0, clone.Count);
array.Add(clone[index]);
clone.RemoveAt(index);
}
}
/// <summary>
/// Switches the positions of two elements in an array
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="array"></param>
/// <param name="first"></param>
/// <param name="second"></param>
public static void Switch<T>(IList<T> array, int first, int second)
{
T obj = array[first];
array[first] = array[second];
array[second] = obj;
}
/// <summary>
/// Checks to see if the contents of two byte arrays are equal
/// </summary>
/// <param name="array1">The first array</param>
/// <param name="array2">The second array</param>
/// <returns>True if the arrays are equal, false if they aren't</returns>
public static bool ByteMatch(byte[] array1, byte[] array2)
{
if (array1 == null)
throw new ArgumentNullException("array1");
if (array2 == null)
throw new ArgumentNullException("array2");
if (array1.Length != array2.Length)
return false;
return ByteMatch(array1, 0, array2, 0, array1.Length);
}
/// <summary>
/// Checks to see if the contents of two byte arrays are equal
/// </summary>
/// <param name="array1">The first array</param>
/// <param name="array2">The second array</param>
/// <param name="offset1">The starting index for the first array</param>
/// <param name="offset2">The starting index for the second array</param>
/// <param name="count">The number of bytes to check</param>
/// <returns></returns>
public static bool ByteMatch(byte[] array1, int offset1, byte[] array2, int offset2, int count)
{
if (array1 == null)
throw new ArgumentNullException("array1");
if (array2 == null)
throw new ArgumentNullException("array2");
// If either of the arrays is too small, they're not equal
if ((array1.Length - offset1) < count || (array2.Length - offset2) < count)
return false;
// Check if any elements are unequal
for (int i = 0; i < count; i++)
if (array1[offset1 + i] != array2[offset2 + i])
return false;
return true;
}
}
}

@ -0,0 +1,885 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using MonoTorrent.BEncoding;
using MonoTorrent.Common;
namespace MonoTorrent
{
/// <summary>
/// The "Torrent" class for both Tracker and Client should inherit from this
/// as it contains the fields that are common to both.
/// </summary>
public class Torrent : IEquatable<Torrent>
{
#region Private Fields
private BEncodedDictionary originalDictionary;
private BEncodedValue azureusProperties;
private IList<RawTrackerTier> announceUrls;
private string comment;
private string createdBy;
private DateTime creationDate;
private byte[] ed2k;
private string encoding;
internal InfoHash infoHash;
private bool isPrivate;
protected string name;
private BEncodedList nodes;
protected int pieceLength;
protected Hashes pieces;
private string publisher;
private string publisherUrl;
private byte[] sha1;
protected long size;
private string source;
protected TorrentFile[] torrentFiles;
protected string torrentPath;
private List<string> getRightHttpSeeds;
private byte[] metadata;
#endregion Private Fields
#region Properties
internal byte[] Metadata
{
get { return this.metadata; }
}
/// <summary>
/// The announce URLs contained within the .torrent file
/// </summary>
public IList<RawTrackerTier> AnnounceUrls
{
get { return this.announceUrls; }
}
/// <summary>
/// This dictionary is specific for azureus client
/// It can contain
/// dht_backup_enable (number)
/// Content (dictionnary)
/// Publisher
/// Description
/// Title
/// Creation Date
/// Content Hash
/// Revision Date
/// Thumbnail (string) = Base64 encoded image
/// Progressive
/// Speed Bps (number)
/// but not useful for MT
/// </summary>
public BEncodedValue AzureusProperties
{
get { return this.azureusProperties; }
}
/// <summary>
/// The comment contained within the .torrent file
/// </summary>
public string Comment
{
get { return this.comment; }
}
/// <summary>
/// The optional string showing who/what created the .torrent
/// </summary>
public string CreatedBy
{
get { return this.createdBy; }
}
/// <summary>
/// The creation date of the .torrent file
/// </summary>
public DateTime CreationDate
{
get { return this.creationDate; }
}
/// <summary>
/// The optional ED2K hash contained within the .torrent file
/// </summary>
public byte[] ED2K
{
get { return this.ed2k; }
}
/// <summary>
/// The encoding used by the client that created the .torrent file
/// </summary>
public string Encoding
{
get { return this.encoding; }
}
/// <summary>
/// The list of files contained within the .torrent which are available for download
/// </summary>
public TorrentFile[] Files
{
get { return this.torrentFiles; }
}
/// <summary>
/// This is the infohash that is generated by putting the "Info" section of a .torrent
/// through a ManagedSHA1 hasher.
/// </summary>
public InfoHash InfoHash
{
get { return this.infoHash; }
}
/// <summary>
/// Shows whether DHT is allowed or not. If it is a private torrent, no peer
/// sharing should be allowed.
/// </summary>
public bool IsPrivate
{
get { return this.isPrivate; }
}
/// <summary>
/// In the case of a single file torrent, this is the name of the file.
/// In the case of a multi file torrent, it is the name of the root folder.
/// </summary>
public string Name
{
get { return this.name; }
private set { this.name = value; }
}
/// <summary>
/// FIXME: No idea what this is.
/// </summary>
public BEncodedList Nodes
{
get { return this.nodes; }
}
/// <summary>
/// The length of each piece in bytes.
/// </summary>
public int PieceLength
{
get { return this.pieceLength; }
}
/// <summary>
/// This is the array of hashes contained within the torrent.
/// </summary>
public Hashes Pieces
{
get { return this.pieces; }
}
/// <summary>
/// The name of the Publisher
/// </summary>
public string Publisher
{
get { return this.publisher; }
}
/// <summary>
/// The Url of the publisher of either the content or the .torrent file
/// </summary>
public string PublisherUrl
{
get { return this.publisherUrl; }
}
/// <summary>
/// The optional SHA1 hash contained within the .torrent file
/// </summary>
public byte[] SHA1
{
get { return this.sha1; }
}
/// <summary>
/// The total size of all the files that have to be downloaded.
/// </summary>
public long Size
{
get { return this.size; }
private set { this.size = value; }
}
/// <summary>
/// The source of the .torrent file
/// </summary>
public string Source
{
get { return this.source; }
}
/// <summary>
/// This is the path at which the .torrent file is located
/// </summary>
public string TorrentPath
{
get { return this.torrentPath; }
internal set { this.torrentPath = value; }
}
/// <summary>
/// This is the http-based seeding (getright protocole)
/// </summary>
public List<string> GetRightHttpSeeds
{
get { return this.getRightHttpSeeds; }
}
#endregion Properties
#region Constructors
protected Torrent()
{
this.announceUrls = new RawTrackerTiers ();
this.comment = string.Empty;
this.createdBy = string.Empty;
this.creationDate = new DateTime(1970, 1, 1, 0, 0, 0);
this.encoding = string.Empty;
this.name = string.Empty;
this.publisher = string.Empty;
this.publisherUrl = string.Empty;
this.source = string.Empty;
this.getRightHttpSeeds = new List<string>();
}
#endregion
#region Public Methods
public override bool Equals(object obj)
{
return this.Equals(obj as Torrent);
}
public bool Equals(Torrent other)
{
if (other == null)
return false;
return this.infoHash == other.infoHash;
}
public override int GetHashCode()
{
return this.infoHash.GetHashCode();
}
internal byte [] ToBytes ()
{
return this.originalDictionary.Encode ();
}
internal BEncodedDictionary ToDictionary ()
{
// Give the user a copy of the original dictionary.
return BEncodedValue.Clone (this.originalDictionary);
}
public override string ToString()
{
return this.name;
}
#endregion Public Methods
#region Private Methods
/// <summary>
/// This method is called internally to read out the hashes from the info section of the
/// .torrent file.
/// </summary>
/// <param name="data">The byte[]containing the hashes from the .torrent file</param>
private void LoadHashPieces(byte[] data)
{
if (data.Length % 20 != 0)
throw new TorrentException("Invalid infohash detected");
this.pieces = new Hashes(data, data.Length / 20);
}
/// <summary>
/// This method is called internally to load in all the files found within the "Files" section
/// of the .torrents infohash
/// </summary>
/// <param name="list">The list containing the files available to download</param>
private void LoadTorrentFiles(BEncodedList list)
{
List<TorrentFile> files = new List<TorrentFile>();
int endIndex;
long length;
string path;
byte[] md5sum;
byte[] ed2k;
byte[] sha1;
int startIndex;
StringBuilder sb = new StringBuilder(32);
foreach (BEncodedDictionary dict in list)
{
length = 0;
path = null;
md5sum = null;
ed2k = null;
sha1 = null;
foreach (KeyValuePair<BEncodedString, BEncodedValue> keypair in dict)
{
switch (keypair.Key.Text)
{
case ("sha1"):
sha1 = ((BEncodedString)keypair.Value).TextBytes;
break;
case ("ed2k"):
ed2k = ((BEncodedString)keypair.Value).TextBytes;
break;
case ("length"):
length = long.Parse(keypair.Value.ToString());
break;
case ("path.utf-8"):
foreach (BEncodedString str in ((BEncodedList)keypair.Value))
{
sb.Append(str.Text);
sb.Append(Path.DirectorySeparatorChar);
}
path = sb.ToString(0, sb.Length - 1);
sb.Remove(0, sb.Length);
break;
case ("path"):
if (string.IsNullOrEmpty(path))
{
foreach (BEncodedString str in ((BEncodedList)keypair.Value))
{
sb.Append(str.Text);
sb.Append(Path.DirectorySeparatorChar);
}
path = sb.ToString(0, sb.Length - 1);
sb.Remove(0, sb.Length);
}
break;
case ("md5sum"):
md5sum = ((BEncodedString)keypair.Value).TextBytes;
break;
default:
break; //FIXME: Log unknown values
}
}
// A zero length file always belongs to the same piece as the previous file
if (length == 0)
{
if (files.Count > 0)
{
startIndex = files[files.Count - 1].EndPieceIndex;
endIndex = files[files.Count - 1].EndPieceIndex;
}
else
{
startIndex = 0;
endIndex = 0;
}
}
else
{
startIndex = (int)(this.size / this.pieceLength);
endIndex = (int)((this.size + length) / this.pieceLength);
if ((this.size + length) % this.pieceLength == 0)
endIndex--;
}
this.size += length;
files.Add(new TorrentFile(path, length, path, startIndex, endIndex, md5sum, ed2k, sha1));
}
this.torrentFiles = files.ToArray();
}
/// <summary>
/// This method is called internally to load the information found within the "Info" section
/// of the .torrent file
/// </summary>
/// <param name="dictionary">The dictionary representing the Info section of the .torrent file</param>
private void ProcessInfo(BEncodedDictionary dictionary)
{
this.metadata = dictionary.Encode();
this.pieceLength = int.Parse(dictionary["piece length"].ToString());
this.LoadHashPieces(((BEncodedString)dictionary["pieces"]).TextBytes);
foreach (KeyValuePair<BEncodedString, BEncodedValue> keypair in dictionary)
{
switch (keypair.Key.Text)
{
case ("source"):
this.source = keypair.Value.ToString();
break;
case ("sha1"):
this.sha1 = ((BEncodedString)keypair.Value).TextBytes;
break;
case ("ed2k"):
this.ed2k = ((BEncodedString)keypair.Value).TextBytes;
break;
case ("publisher-url.utf-8"):
if (keypair.Value.ToString().Length > 0)
this.publisherUrl = keypair.Value.ToString();
break;
case ("publisher-url"):
if ((String.IsNullOrEmpty(this.publisherUrl)) && (keypair.Value.ToString().Length > 0))
this.publisherUrl = keypair.Value.ToString();
break;
case ("publisher.utf-8"):
if (keypair.Value.ToString().Length > 0)
this.publisher = keypair.Value.ToString();
break;
case ("publisher"):
if ((String.IsNullOrEmpty(this.publisher)) && (keypair.Value.ToString().Length > 0))
this.publisher = keypair.Value.ToString();
break;
case ("files"):
this.LoadTorrentFiles(((BEncodedList)keypair.Value));
break;
case ("name.utf-8"):
if (keypair.Value.ToString().Length > 0)
this.name = keypair.Value.ToString();
break;
case ("name"):
if ((String.IsNullOrEmpty(this.name)) && (keypair.Value.ToString().Length > 0))
this.name = keypair.Value.ToString();
break;
case ("piece length"): // Already handled
break;
case ("length"):
break; // This is a singlefile torrent
case ("private"):
this.isPrivate = (keypair.Value.ToString() == "1") ? true : false;
break;
default:
break;
}
}
if (this.torrentFiles == null) // Not a multi-file torrent
{
long length = long.Parse(dictionary["length"].ToString());
this.size = length;
string path = this.name;
byte[] md5 = (dictionary.ContainsKey("md5")) ? ((BEncodedString)dictionary["md5"]).TextBytes : null;
byte[] ed2k = (dictionary.ContainsKey("ed2k")) ? ((BEncodedString)dictionary["ed2k"]).TextBytes : null;
byte[] sha1 = (dictionary.ContainsKey("sha1")) ? ((BEncodedString)dictionary["sha1"]).TextBytes : null;
this.torrentFiles = new TorrentFile[1];
int endPiece = Math.Min(this.Pieces.Count - 1, (int)((this.size + (this.pieceLength - 1)) / this.pieceLength));
this.torrentFiles[0] = new TorrentFile(path, length, path, 0, endPiece, md5, ed2k, sha1);
}
}
#endregion Private Methods
#region Loading methods
/// <summary>
/// This method loads a .torrent file from the specified path.
/// </summary>
/// <param name="path">The path to load the .torrent file from</param>
public static Torrent Load(string path)
{
Check.Path(path);
using (Stream s = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
return Torrent.Load(s, path);
}
/// <summary>
/// Loads a torrent from a byte[] containing the bencoded data
/// </summary>
/// <param name="data">The byte[] containing the data</param>
/// <returns></returns>
public static Torrent Load(byte[] data)
{
Check.Data(data);
using (MemoryStream s = new MemoryStream(data))
return Load(s, "");
}
/// <summary>
/// Loads a .torrent from the supplied stream
/// </summary>
/// <param name="stream">The stream containing the data to load</param>
/// <returns></returns>
public static Torrent Load(Stream stream)
{
Check.Stream(stream);
if (stream == null)
throw new ArgumentNullException("stream");
return Torrent.Load(stream, "");
}
/// <summary>
/// Loads a .torrent file from the specified URL
/// </summary>
/// <param name="url">The URL to download the .torrent from</param>
/// <param name="location">The path to download the .torrent to before it gets loaded</param>
/// <returns></returns>
public static Torrent Load(Uri url, string location)
{
Check.Url(url);
Check.Location(location);
try
{
using (WebClient client = new WebClient())
client.DownloadFile(url, location);
}
catch (Exception ex)
{
throw new TorrentException("Could not download .torrent file from the specified url", ex);
}
return Torrent.Load(location);
}
/// <summary>
/// Loads a .torrent from the specificed path. A return value indicates
/// whether the operation was successful.
/// </summary>
/// <param name="path">The path to load the .torrent file from</param>
/// <param name="torrent">If the loading was succesful it is assigned the Torrent</param>
/// <returns>True if successful</returns>
public static bool TryLoad(string path, out Torrent torrent)
{
Check.Path(path);
try
{
torrent = Torrent.Load(path);
}
catch
{
torrent = null;
}
return torrent != null;
}
/// <summary>
/// Loads a .torrent from the specified byte[]. A return value indicates
/// whether the operation was successful.
/// </summary>
/// <param name="data">The byte[] to load the .torrent from</param>
/// <param name="torrent">If loading was successful, it contains the Torrent</param>
/// <returns>True if successful</returns>
public static bool TryLoad(byte[] data, out Torrent torrent)
{
Check.Data(data);
try
{
torrent = Torrent.Load(data);
}
catch
{
torrent = null;
}
return torrent != null;
}
/// <summary>
/// Loads a .torrent from the supplied stream. A return value indicates
/// whether the operation was successful.
/// </summary>
/// <param name="stream">The stream containing the data to load</param>
/// <param name="torrent">If the loading was succesful it is assigned the Torrent</param>
/// <returns>True if successful</returns>
public static bool TryLoad(Stream stream, out Torrent torrent)
{
Check.Stream(stream);
try
{
torrent = Torrent.Load(stream);
}
catch
{
torrent = null;
}
return torrent != null;
}
/// <summary>
/// Loads a .torrent file from the specified URL. A return value indicates
/// whether the operation was successful.
/// </summary>
/// <param name="url">The URL to download the .torrent from</param>
/// <param name="location">The path to download the .torrent to before it gets loaded</param>
/// <param name="torrent">If the loading was succesful it is assigned the Torrent</param>
/// <returns>True if successful</returns>
public static bool TryLoad(Uri url, string location, out Torrent torrent)
{
Check.Url(url);
Check.Location(location);
try
{
torrent = Torrent.Load(url, location);
}
catch
{
torrent = null;
}
return torrent != null;
}
/// <summary>
/// Called from either Load(stream) or Load(string).
/// </summary>
/// <param name="stream"></param>
/// <param name="path"></param>
/// <returns></returns>
private static Torrent Load(Stream stream, string path)
{
Check.Stream(stream);
Check.Path(path);
try
{
Torrent t = Torrent.LoadCore ((BEncodedDictionary) BEncodedDictionary.Decode(stream));
t.torrentPath = path;
return t;
}
catch (BEncodingException ex)
{
throw new TorrentException("Invalid torrent file specified", ex);
}
}
public static Torrent Load(BEncodedDictionary torrentInformation)
{
return LoadCore ((BEncodedDictionary)BEncodedValue.Decode (torrentInformation.Encode ()));
}
internal static Torrent LoadCore(BEncodedDictionary torrentInformation)
{
Check.TorrentInformation(torrentInformation);
Torrent t = new Torrent();
t.LoadInternal(torrentInformation);
return t;
}
protected void LoadInternal(BEncodedDictionary torrentInformation)
{
Check.TorrentInformation(torrentInformation);
this.originalDictionary = torrentInformation;
this.torrentPath = "";
try
{
foreach (KeyValuePair<BEncodedString, BEncodedValue> keypair in torrentInformation)
{
switch (keypair.Key.Text)
{
case ("announce"):
// Ignore this if we have an announce-list
if (torrentInformation.ContainsKey("announce-list"))
break;
this.announceUrls.Add(new RawTrackerTier ());
this.announceUrls[0].Add(keypair.Value.ToString());
break;
case ("creation date"):
try
{
try
{
this.creationDate = this.creationDate.AddSeconds(long.Parse(keypair.Value.ToString()));
}
catch (Exception e)
{
if (e is ArgumentOutOfRangeException)
this.creationDate = this.creationDate.AddMilliseconds(long.Parse(keypair.Value.ToString()));
else
throw;
}
}
catch (Exception e)
{
if (e is ArgumentOutOfRangeException)
throw new BEncodingException("Argument out of range exception when adding seconds to creation date.", e);
else if (e is FormatException)
throw new BEncodingException(String.Format("Could not parse {0} into a number", keypair.Value), e);
else
throw;
}
break;
case ("nodes"):
this.nodes = (BEncodedList)keypair.Value;
break;
case ("comment.utf-8"):
if (keypair.Value.ToString().Length != 0)
this.comment = keypair.Value.ToString(); // Always take the UTF-8 version
break; // even if there's an existing value
case ("comment"):
if (String.IsNullOrEmpty(this.comment))
this.comment = keypair.Value.ToString();
break;
case ("publisher-url.utf-8"): // Always take the UTF-8 version
this.publisherUrl = keypair.Value.ToString(); // even if there's an existing value
break;
case ("publisher-url"):
if (String.IsNullOrEmpty(this.publisherUrl))
this.publisherUrl = keypair.Value.ToString();
break;
case ("azureus_properties"):
this.azureusProperties = keypair.Value;
break;
case ("created by"):
this.createdBy = keypair.Value.ToString();
break;
case ("encoding"):
this.encoding = keypair.Value.ToString();
break;
case ("info"):
using (SHA1 s = HashAlgoFactory.Create<SHA1>())
this.infoHash = new InfoHash (s.ComputeHash(keypair.Value.Encode()));
this.ProcessInfo(((BEncodedDictionary)keypair.Value));
break;
case ("name"): // Handled elsewhere
break;
case ("announce-list"):
if (keypair.Value is BEncodedString)
break;
BEncodedList announces = (BEncodedList)keypair.Value;
for (int j = 0; j < announces.Count; j++)
{
if (announces[j] is BEncodedList)
{
BEncodedList bencodedTier = (BEncodedList)announces[j];
List<string> tier = new List<string>(bencodedTier.Count);
for (int k = 0; k < bencodedTier.Count; k++)
tier.Add(bencodedTier[k].ToString());
Toolbox.Randomize<string>(tier);
RawTrackerTier collection = new RawTrackerTier ();
for (int k = 0; k < tier.Count; k++)
collection.Add(tier[k]);
if (collection.Count != 0)
this.announceUrls.Add(collection);
}
else
{
throw new BEncodingException(String.Format("Non-BEncodedList found in announce-list (found {0})",
announces[j].GetType()));
}
}
break;
case ("httpseeds"):
// This form of web-seeding is not supported.
break;
case ("url-list"):
if (keypair.Value is BEncodedString)
{
this.getRightHttpSeeds.Add(((BEncodedString)keypair.Value).Text);
}
else if (keypair.Value is BEncodedList)
{
foreach (BEncodedString str in (BEncodedList)keypair.Value)
this.GetRightHttpSeeds.Add(str.Text);
}
break;
default:
break;
}
}
}
catch (Exception e)
{
if (e is BEncodingException)
throw;
else
throw new BEncodingException("", e);
}
}
#endregion Loading methods
}
}

@ -0,0 +1,28 @@
using System;
namespace MonoTorrent
{
[Serializable]
public class TorrentException : Exception
{
public TorrentException()
: base()
{
}
public TorrentException(string message)
: base(message)
{
}
public TorrentException(string message, Exception innerException)
: base(message, innerException)
{
}
public TorrentException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
: base(info, context)
{
}
}
}

@ -0,0 +1,205 @@
using System;
using System.Text;
namespace MonoTorrent
{
/// <summary>
/// This is the base class for the files available to download from within a .torrent.
/// This should be inherited by both Client and Tracker "TorrentFile" classes
/// </summary>
public class TorrentFile : IEquatable<TorrentFile>
{
#region Private Fields
private BitField bitfield;
private BitField selector;
private byte[] ed2k;
private int endPiece;
private string fullPath;
private long length;
private byte[] md5;
private string path;
private Priority priority;
private byte[] sha1;
private int startPiece;
#endregion Private Fields
#region Member Variables
/// <summary>
/// The number of pieces which have been successfully downloaded which are from this file
/// </summary>
public BitField BitField
{
get { return this.bitfield; }
}
public long BytesDownloaded
{
get { return (long)(this.BitField.PercentComplete * this.Length / 100.0); }
}
/// <summary>
/// The ED2K hash of the file
/// </summary>
public byte[] ED2K
{
get { return this.ed2k; }
}
/// <summary>
/// The index of the last piece of this file
/// </summary>
public int EndPieceIndex
{
get { return this.endPiece; }
}
public string FullPath
{
get { return this.fullPath; }
internal set { this.fullPath = value; }
}
/// <summary>
/// The length of the file in bytes
/// </summary>
public long Length
{
get { return this.length; }
}
/// <summary>
/// The MD5 hash of the file
/// </summary>
public byte[] MD5
{
get { return this.md5; }
internal set { this.md5 = value; }
}
/// <summary>
/// In the case of a single torrent file, this is the name of the file.
/// In the case of a multi-file torrent this is the relative path of the file
/// (including the filename) from the base directory
/// </summary>
public string Path
{
get { return this.path; }
}
/// <summary>
/// The priority of this torrent file
/// </summary>
public Priority Priority
{
get { return this.priority; }
set { this.priority = value; }
}
/// <summary>
/// The SHA1 hash of the file
/// </summary>
public byte[] SHA1
{
get { return this.sha1; }
}
/// <summary>
/// The index of the first piece of this file
/// </summary>
public int StartPieceIndex
{
get { return this.startPiece; }
}
#endregion
#region Constructors
public TorrentFile(string path, long length)
: this(path, length, path)
{
}
public TorrentFile (string path, long length, string fullPath)
: this (path, length, fullPath, 0, 0)
{
}
public TorrentFile (string path, long length, int startIndex, int endIndex)
: this (path, length, path, startIndex, endIndex)
{
}
public TorrentFile(string path, long length, string fullPath, int startIndex, int endIndex)
: this(path, length, fullPath, startIndex, endIndex, null, null, null)
{
}
public TorrentFile(string path, long length, string fullPath, int startIndex, int endIndex, byte[] md5, byte[] ed2k, byte[] sha1)
{
this.bitfield = new BitField(endIndex - startIndex + 1);
this.ed2k = ed2k;
this.endPiece = endIndex;
this.fullPath = fullPath;
this.length = length;
this.md5 = md5;
this.path = path;
this.priority = Priority.Normal;
this.sha1 = sha1;
this.startPiece = startIndex;
}
#endregion
#region Methods
public override bool Equals(object obj)
{
return this.Equals(obj as TorrentFile);
}
public bool Equals(TorrentFile other)
{
return other == null ? false : this.path == other.path && this.length == other.length; ;
}
public override int GetHashCode()
{
return this.path.GetHashCode();
}
internal BitField GetSelector(int totalPieces)
{
if (this.selector != null)
return this.selector;
this.selector = new BitField(totalPieces);
for (int i = this.StartPieceIndex; i <= this.EndPieceIndex; i++)
this.selector[i] = true;
return this.selector;
}
public override string ToString()
{
StringBuilder sb = new StringBuilder(32);
sb.Append("File: ");
sb.Append(this.path);
sb.Append(" StartIndex: ");
sb.Append(this.StartPieceIndex);
sb.Append(" EndIndex: ");
sb.Append(this.EndPieceIndex);
return sb.ToString();
}
#endregion Methods
}
}

@ -0,0 +1,153 @@
//
// System.Web.HttpUtility/HttpEncoder
//
// Authors:
// Patrik Torstensson (Patrik.Torstensson@labs2.com)
// Wictor Wilén (decode/encode functions) (wictor@ibizkit.se)
// Tim Coleman (tim@timcoleman.com)
using System;
using System.Text;
using System.IO;
using System.Collections.Generic;
namespace MonoTorrent
{
static class UriHelper
{
static readonly char [] hexChars = "0123456789abcdef".ToCharArray ();
public static string UrlEncode (byte[] bytes)
{
if (bytes == null)
throw new ArgumentNullException ("bytes");
var result = new MemoryStream (bytes.Length);
for (int i = 0; i < bytes.Length; i++)
UrlEncodeChar ((char)bytes [i], result, false);
return Encoding.ASCII.GetString (result.ToArray());
}
public static byte [] UrlDecode (string s)
{
if (null == s)
return null;
var e = Encoding.UTF8;
if (s.IndexOf ('%') == -1 && s.IndexOf ('+') == -1)
return e.GetBytes (s);
long len = s.Length;
var bytes = new List <byte> ();
int xchar;
char ch;
for (int i = 0; i < len; i++) {
ch = s [i];
if (ch == '%' && i + 2 < len && s [i + 1] != '%') {
if (s [i + 1] == 'u' && i + 5 < len) {
// unicode hex sequence
xchar = GetChar (s, i + 2, 4);
if (xchar != -1) {
WriteCharBytes (bytes, (char)xchar, e);
i += 5;
} else
WriteCharBytes (bytes, '%', e);
} else if ((xchar = GetChar (s, i + 1, 2)) != -1) {
WriteCharBytes (bytes, (char)xchar, e);
i += 2;
} else {
WriteCharBytes (bytes, '%', e);
}
continue;
}
if (ch == '+')
WriteCharBytes (bytes, ' ', e);
else
WriteCharBytes (bytes, ch, e);
}
return bytes.ToArray ();
}
static void UrlEncodeChar (char c, Stream result, bool isUnicode) {
if (c > ' ' && NotEncoded (c)) {
result.WriteByte ((byte)c);
return;
}
if (c==' ') {
result.WriteByte ((byte)'+');
return;
}
if ( (c < '0') ||
(c < 'A' && c > '9') ||
(c > 'Z' && c < 'a') ||
(c > 'z')) {
if (isUnicode && c > 127) {
result.WriteByte ((byte)'%');
result.WriteByte ((byte)'u');
result.WriteByte ((byte)'0');
result.WriteByte ((byte)'0');
}
else
result.WriteByte ((byte)'%');
int idx = ((int) c) >> 4;
result.WriteByte ((byte)hexChars [idx]);
idx = ((int) c) & 0x0F;
result.WriteByte ((byte)hexChars [idx]);
}
else {
result.WriteByte ((byte)c);
}
}
static int GetChar (string str, int offset, int length)
{
int val = 0;
int end = length + offset;
for (int i = offset; i < end; i++) {
char c = str [i];
if (c > 127)
return -1;
int current = GetInt ((byte) c);
if (current == -1)
return -1;
val = (val << 4) + current;
}
return val;
}
static int GetInt (byte b)
{
char c = (char) b;
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return -1;
}
static bool NotEncoded (char c)
{
return c == '!' || c == '(' || c == ')' || c == '*' || c == '-' || c == '.' || c == '_' || c == '\'';
}
static void WriteCharBytes (List<byte> buf, char ch, Encoding e)
{
if (ch > 255) {
foreach (byte b in e.GetBytes (new char[] { ch }))
buf.Add (b);
} else
buf.Add ((byte)ch);
}
}
}

@ -19,5 +19,6 @@ namespace NzbDrone.Api.Config
public String ChownGroup { get; set; }
public Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
public Boolean CopyUsingHardlinks { get; set; }
}
}

@ -162,6 +162,72 @@ namespace NzbDrone.Common.Test.DiskProviderTests
Directory.Exists(sourceDir).Should().BeFalse();
}
[Test]
public void should_be_able_to_hardlink_file()
{
var sourceDir = GetTempFilePath();
var source = Path.Combine(sourceDir, "test.txt");
var destination = Path.Combine(sourceDir, "destination.txt");
Directory.CreateDirectory(sourceDir);
Subject.WriteAllText(source, "SourceFile");
var result = Subject.TransferFile(source, destination, TransferMode.HardLink);
result.Should().Be(TransferMode.HardLink);
File.AppendAllText(source, "Test");
File.ReadAllText(destination).Should().Be("SourceFileTest");
}
private void DoHardLinkRename(FileShare fileShare)
{
var sourceDir = GetTempFilePath();
var source = Path.Combine(sourceDir, "test.txt");
var destination = Path.Combine(sourceDir, "destination.txt");
var rename = Path.Combine(sourceDir, "rename.txt");
Directory.CreateDirectory(sourceDir);
Subject.WriteAllText(source, "SourceFile");
Subject.TransferFile(source, destination, TransferMode.HardLink);
using (var stream = new FileStream(source, FileMode.Open, FileAccess.Read, fileShare))
{
stream.ReadByte();
Subject.MoveFile(destination, rename);
stream.ReadByte();
}
File.Exists(rename).Should().BeTrue();
File.Exists(destination).Should().BeFalse();
File.AppendAllText(source, "Test");
File.ReadAllText(rename).Should().Be("SourceFileTest");
}
[Test]
public void should_be_able_to_rename_open_hardlinks_with_fileshare_delete()
{
DoHardLinkRename(FileShare.Delete);
}
[Test]
public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_none()
{
Assert.Throws<IOException>(() => DoHardLinkRename(FileShare.None));
}
[Test]
public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_write()
{
Assert.Throws<IOException>(() => DoHardLinkRename(FileShare.Read));
}
[Test]
public void empty_folder_should_return_folder_modified_date()
{

@ -11,12 +11,16 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestFixture]
public class CleanseLogMessageFixture
{
[TestCase(@"http://127.0.0.1:1234/api/call?vv=1&apikey=mySecret")]
[TestCase(@"http://127.0.0.1:1234/api/call?vv=1&ma_username=mySecret&ma_password=mySecret")]
// Indexer Urls
[TestCase(@"https://iptorrents.com/torrents/rss?u=mySecret;tp=mySecret;l5;download")]
[TestCase(@"http://rss.torrentleech.org/mySecret")]
[TestCase(@"http://www.bitmetv.org/rss.php?uid=mySecret&passkey=mySecret")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
[TestCase(@"{ ""Name"" : ""Server1.Username"", ""Value"" : ""mySecret"" }, { ""Name"" : ""Server1.Password"", ""Value"" : ""mySecret"" }, ")]
// Sabnzbd
[TestCase(@"http://127.0.0.1:1234/api/call?vv=1&apikey=mySecret")]
[TestCase(@"http://127.0.0.1:1234/api/call?vv=1&ma_username=mySecret&ma_password=mySecret")]
[TestCase(@"""config"":{""newzbin"":{""username"":""mySecret"",""password"":""mySecret""}")]
[TestCase(@"""nzbxxx"":{""username"":""mySecret"",""apikey"":""mySecret""}")]
[TestCase(@"""growl"":{""growl_password"":""mySecret"",""growl_server"":""""}")]
@ -24,6 +28,19 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"""misc"":{""username"":""mySecret"",""api_key"":""mySecret"",""password"":""mySecret"",""nzb_key"":""mySecret""}")]
[TestCase(@"""servers"":[{""username"":""mySecret"",""password"":""mySecret""}]")]
[TestCase(@"""misc"":{""email_account"":""mySecret"",""email_to"":[],""email_from"":"""",""email_pwd"":""mySecret""}")]
// uTorrent
[TestCase(@"http://localhost:9091/gui/?token=wThmph5l0ZXfH-a6WOA4lqiLvyjCP0FpMrMeXmySecret_VXBO11HoKL751MAAAAA&list=1")]
[TestCase(@",[""boss_key"",0,""mySecret"",{""access"":""Y""}],[""boss_key_salt"",0,""mySecret"",{""access"":""W""}]")]
[TestCase(@",[""webui.username"",2,""mySecret"",{""access"":""Y""}],[""webui.password"",2,""mySecret"",{""access"":""Y""}]")]
[TestCase(@",[""webui.uconnect_username"",2,""mySecret"",{""access"":""Y""}],[""webui.uconnect_password"",2,""mySecret"",{""access"":""Y""}]")]
[TestCase(@",[""proxy.proxy"",2,""mySecret"",{""access"":""Y""}]")]
[TestCase(@",[""proxy.username"",2,""mySecret"",{""access"":""Y""}],[""proxy.password"",2,""mySecret"",{""access"":""Y""}]")]
// Deluge
[TestCase(@",{""download_location"": ""C:\Users\\mySecret mySecret\\Downloads""}")]
[TestCase(@",{""download_location"": ""/home/mySecret/Downloads""}")]
// BroadcastheNet
[TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]
[TestCase(@"""DownloadURL"":""https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
public void should_clean_message(String message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);

@ -80,6 +80,7 @@
<Compile Include="Http\HttpRequestFixture.cs" />
<Compile Include="InstrumentationTests\CleanseLogMessageFixture.cs" />
<Compile Include="LevenshteinDistanceFixture.cs" />
<Compile Include="OsPathFixture.cs" />
<Compile Include="PathExtensionFixture.cs" />
<Compile Include="ProcessProviderTests.cs" />
<Compile Include="ReflectionExtensions.cs" />

@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Test.Common;
using FluentAssertions;
namespace NzbDrone.Common.Test
{
public class OsPathFixture : TestBase
{
[TestCase(@"C:\rooted\windows\path\", OsPathKind.Windows)]
[TestCase(@"C:\rooted\windows\path", OsPathKind.Windows)]
[TestCase(@"C:\", OsPathKind.Windows)]
[TestCase(@"C:", OsPathKind.Windows)]
[TestCase(@"\\rooted\unc\path\", OsPathKind.Windows)]
[TestCase(@"\\rooted\unc\path", OsPathKind.Windows)]
[TestCase(@"\relative\windows\path\", OsPathKind.Windows)]
[TestCase(@"\relative\windows\path", OsPathKind.Windows)]
[TestCase(@"relative\windows\path\", OsPathKind.Windows)]
[TestCase(@"relative\windows\path", OsPathKind.Windows)]
[TestCase(@"relative\", OsPathKind.Windows)]
[TestCase(@"relative", OsPathKind.Unknown)]
[TestCase("/rooted/linux/path/", OsPathKind.Unix)]
[TestCase("/rooted/linux/path", OsPathKind.Unix)]
[TestCase("/", OsPathKind.Unix)]
[TestCase("linux/path", OsPathKind.Unix)]
public void should_auto_detect_kind(String path, OsPathKind kind)
{
var result = new OsPath(path);
result.Kind.Should().Be(kind);
if (kind == OsPathKind.Windows)
{
result.IsWindowsPath.Should().BeTrue();
result.IsUnixPath.Should().BeFalse();
}
else if (kind == OsPathKind.Unix)
{
result.IsWindowsPath.Should().BeFalse();
result.IsUnixPath.Should().BeTrue();
}
else
{
result.IsWindowsPath.Should().BeFalse();
result.IsUnixPath.Should().BeFalse();
}
}
[Test]
public void should_add_directory_slash()
{
var osPath = new OsPath(@"C:\rooted\windows\path\");
osPath.Directory.Should().NotBeNull();
osPath.Directory.ToString().Should().Be(@"C:\rooted\windows\");
}
[TestCase(@"C:\rooted\windows\path", @"C:\rooted\windows\")]
[TestCase(@"C:\rooted", @"C:\")]
[TestCase(@"C:", null)]
[TestCase("/rooted/linux/path", "/rooted/linux/")]
[TestCase("/rooted", "/")]
[TestCase("/", null)]
public void should_return_parent_directory(String path, String expectedParent)
{
var osPath = new OsPath(path);
osPath.Directory.Should().NotBeNull();
osPath.Directory.Should().Be(new OsPath(expectedParent));
}
[Test]
public void should_return_empty_as_parent_of_root_unc()
{
var osPath = new OsPath(@"\\unc");
osPath.Directory.IsEmpty.Should().BeTrue();
}
[TestCase(@"C:\rooted\windows\path")]
[TestCase(@"C:")]
[TestCase(@"\\blaat")]
[TestCase("/rooted/linux/path")]
[TestCase("/")]
public void should_detect_rooted_ospaths(String path)
{
var osPath = new OsPath(path);
osPath.IsRooted.Should().BeTrue();
}
[TestCase(@"\rooted\windows\path")]
[TestCase(@"rooted\windows\path")]
[TestCase(@"path")]
[TestCase("linux/path")]
public void should_detect_unrooted_ospaths(String path)
{
var osPath = new OsPath(path);
osPath.IsRooted.Should().BeFalse();
}
[TestCase(@"C:\rooted\windows\path", "path")]
[TestCase(@"C:", "C:")]
[TestCase(@"\\blaat", "blaat")]
[TestCase("/rooted/linux/path", "path")]
[TestCase("/", null)]
[TestCase(@"\rooted\windows\path\", "path")]
[TestCase(@"rooted\windows\path", "path")]
[TestCase(@"path", "path")]
[TestCase("linux/path", "path")]
public void should_return_filename(String path, String expectedFilePath)
{
var osPath = new OsPath(path);
osPath.FileName.Should().Be(expectedFilePath);
}
[Test]
public void should_compare_windows_ospathkind_case_insensitive()
{
var left = new OsPath(@"C:\rooted\Windows\path");
var right = new OsPath(@"C:\rooted\windows\path");
left.Should().Be(right);
}
[Test]
public void should_compare_unix_ospathkind_case_sensitive()
{
var left = new OsPath(@"/rooted/Linux/path");
var right = new OsPath(@"/rooted/linux/path");
left.Should().NotBe(right);
}
[Test]
public void should_not_ignore_trailing_slash_during_compare()
{
var left = new OsPath(@"/rooted/linux/path/");
var right = new OsPath(@"/rooted/linux/path");
left.Should().NotBe(right);
}
[TestCase(@"C:\Test", @"sub", @"C:\Test\sub")]
[TestCase(@"C:\Test", @"sub\test", @"C:\Test\sub\test")]
[TestCase(@"C:\Test\", @"\sub", @"C:\Test\sub")]
[TestCase(@"C:\Test", @"sub\", @"C:\Test\sub\")]
[TestCase(@"C:\Test", @"C:\Test2\sub", @"C:\Test2\sub")]
[TestCase(@"/Test", @"sub", @"/Test/sub")]
[TestCase(@"/Test", @"sub/", @"/Test/sub/")]
[TestCase(@"/Test", @"sub/", @"/Test/sub/")]
[TestCase(@"/Test/", @"sub/test/", @"/Test/sub/test/")]
[TestCase(@"/Test/", @"/Test2/", @"/Test2/")]
[TestCase(@"C:\Test", "", @"C:\Test")]
public void should_combine_path(String left, String right, String expectedResult)
{
var osPathLeft = new OsPath(left);
var osPathRight = new OsPath(right);
var result = osPathLeft + osPathRight;
result.FullPath.Should().Be(expectedResult);
}
[Test]
public void should_fix_slashes_windows()
{
var osPath = new OsPath(@"C:/on/windows/transmission\uses/forward/slashes");
osPath.Kind.Should().Be(OsPathKind.Windows);
osPath.FullPath.Should().Be(@"C:\on\windows\transmission\uses\forward\slashes");
}
[Test]
public void should_fix_slashes_unix()
{
var osPath = new OsPath(@"/just/a/test\to\verify the/slashes\");
osPath.Kind.Should().Be(OsPathKind.Unix);
osPath.FullPath.Should().Be(@"/just/a/test/to/verify the/slashes/");
}
[Test]
public void should_combine_mixed_slashes()
{
var left = new OsPath(@"C:/on/windows/transmission");
var right = new OsPath(@"uses/forward/slashes", OsPathKind.Unknown);
var osPath = left + right;
osPath.Kind.Should().Be(OsPathKind.Windows);
osPath.FullPath.Should().Be(@"C:\on\windows\transmission\uses\forward\slashes");
}
[TestCase(@"C:\Test\Data\", @"C:\Test\Data\Sub\Folder", @"Sub\Folder")]
[TestCase(@"C:\Test\Data\", @"C:\Test\Data2\Sub\Folder", @"..\Data2\Sub\Folder")]
[TestCase(@"/parent/folder", @"/parent/folder/Sub/Folder", @"Sub/Folder")]
public void should_create_relative_path(String parent, String child, String expected)
{
var left = new OsPath(child);
var right = new OsPath(parent);
var osPath = left - right;
osPath.Kind.Should().Be(OsPathKind.Unknown);
osPath.FullPath.Should().Be(expected);
}
[Test]
public void should_parse_null_as_empty()
{
var result = new OsPath(null);
result.FullPath.Should().BeEmpty();
result.IsEmpty.Should().BeTrue();
}
[TestCase(@"C:\Test\", @"C:\Test", true)]
[TestCase(@"C:\Test\", @"C:\Test\Contains\", true)]
[TestCase(@"C:\Test\", @"C:\Other\", false)]
public void should_evaluate_contains(String parent, String child, Boolean expectedResult)
{
var left = new OsPath(parent);
var right = new OsPath(child);
var result = left.Contains(right);
result.Should().Be(expectedResult);
}
}
}

@ -23,5 +23,10 @@ namespace NzbDrone.Common
return merged;
}
public static void Add<TKey, TValue>(this ICollection<KeyValuePair<TKey, TValue>> collection, TKey key, TValue value)
{
collection.Add(key, value);
}
}
}

@ -13,12 +13,6 @@ namespace NzbDrone.Common.Disk
{
public abstract class DiskProviderBase : IDiskProvider
{
enum TransferAction
{
Copy,
Move
}
private static readonly Logger Logger = NzbDroneLogger.GetLogger();
public abstract long? GetAvailableSpace(string path);
@ -152,7 +146,7 @@ namespace NzbDrone.Common.Disk
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
TransferFolder(source, destination, TransferAction.Copy);
TransferFolder(source, destination, TransferMode.Copy);
}
public void MoveFolder(string source, string destination)
@ -162,7 +156,7 @@ namespace NzbDrone.Common.Disk
try
{
TransferFolder(source, destination, TransferAction.Move);
TransferFolder(source, destination, TransferMode.Move);
DeleteFolder(source, true);
}
catch (Exception e)
@ -173,15 +167,15 @@ namespace NzbDrone.Common.Disk
}
}
private void TransferFolder(string source, string target, TransferAction transferAction)
public void TransferFolder(string source, string destination, TransferMode mode)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(target, () => target).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
Logger.ProgressDebug("{0} {1} -> {2}", transferAction, source, target);
Logger.ProgressDebug("{0} {1} -> {2}", mode, source, destination);
var sourceFolder = new DirectoryInfo(source);
var targetFolder = new DirectoryInfo(target);
var targetFolder = new DirectoryInfo(destination);
if (!targetFolder.Exists)
{
@ -190,28 +184,16 @@ namespace NzbDrone.Common.Disk
foreach (var subDir in sourceFolder.GetDirectories())
{
TransferFolder(subDir.FullName, Path.Combine(target, subDir.Name), transferAction);
TransferFolder(subDir.FullName, Path.Combine(destination, subDir.Name), mode);
}
foreach (var sourceFile in sourceFolder.GetFiles("*.*", SearchOption.TopDirectoryOnly))
{
var destFile = Path.Combine(target, sourceFile.Name);
var destFile = Path.Combine(destination, sourceFile.Name);
Logger.ProgressDebug("{0} {1} -> {2}", transferAction, sourceFile, destFile);
Logger.ProgressDebug("{0} {1} -> {2}", mode, sourceFile, destFile);
switch (transferAction)
{
case TransferAction.Copy:
{
sourceFile.CopyTo(destFile, true);
break;
}
case TransferAction.Move:
{
MoveFile(sourceFile.FullName, destFile, true);
break;
}
}
TransferFile(sourceFile.FullName, destFile, mode, true);
}
}
@ -227,19 +209,15 @@ namespace NzbDrone.Common.Disk
public void CopyFile(string source, string destination, bool overwrite = false)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
if (source.PathEquals(destination))
{
Logger.Warn("Source and destination can't be the same {0}", source);
return;
}
File.Copy(source, destination, overwrite);
TransferFile(source, destination, TransferMode.Copy, overwrite);
}
public void MoveFile(string source, string destination, bool overwrite = false)
{
TransferFile(source, destination, TransferMode.Move, overwrite);
}
public TransferMode TransferFile(string source, string destination, TransferMode mode, bool overwrite)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
@ -247,7 +225,7 @@ namespace NzbDrone.Common.Disk
if (source.PathEquals(destination))
{
Logger.Warn("Source and destination can't be the same {0}", source);
return;
return TransferMode.None;
}
if (FileExists(destination) && overwrite)
@ -255,10 +233,37 @@ namespace NzbDrone.Common.Disk
DeleteFile(destination);
}
RemoveReadOnly(source);
File.Move(source, destination);
if (mode.HasFlag(TransferMode.HardLink))
{
bool createdHardlink = TryCreateHardLink(source, destination);
if (createdHardlink)
{
return TransferMode.HardLink;
}
else if (!mode.HasFlag(TransferMode.Copy))
{
throw new IOException("Hardlinking from '" + source + "' to '" + destination + "' failed.");
}
}
if (mode.HasFlag(TransferMode.Copy))
{
File.Copy(source, destination, overwrite);
return TransferMode.Copy;
}
if (mode.HasFlag(TransferMode.Move))
{
RemoveReadOnly(source);
File.Move(source, destination);
return TransferMode.Move;
}
return TransferMode.None;
}
public abstract bool TryCreateHardLink(string source, string destination);
public void DeleteFolder(string path, bool recursive)
{
Ensure.That(path, () => path).IsValidPath();

@ -25,9 +25,12 @@ namespace NzbDrone.Common.Disk
void CreateFolder(string path);
void CopyFolder(string source, string destination);
void MoveFolder(string source, string destination);
void TransferFolder(string source, string destination, TransferMode transferMode);
void DeleteFile(string path);
void CopyFile(string source, string destination, bool overwrite = false);
void MoveFile(string source, string destination, bool overwrite = false);
TransferMode TransferFile(string source, string destination, TransferMode transferMode, bool overwrite = false);
bool TryCreateHardLink(string source, string destination);
void DeleteFolder(string path, bool recursive);
string ReadAllText(string filePath);
void WriteAllText(string filename, string contents);

@ -0,0 +1,406 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Common.Disk
{
public struct OsPath : IEquatable<OsPath>
{
private readonly String _path;
private readonly OsPathKind _kind;
public OsPath(String path)
{
if (path == null)
{
_kind = OsPathKind.Unknown;
_path = String.Empty;
}
else
{
_kind = DetectPathKind(path);
_path = FixSlashes(path, _kind);
}
}
public OsPath(String path, OsPathKind kind)
{
if (path == null)
{
_kind = kind;
_path = String.Empty;
}
else
{
_kind = kind;
_path = FixSlashes(path, kind);
}
}
private static OsPathKind DetectPathKind(String path)
{
if (path.StartsWith("/"))
{
return OsPathKind.Unix;
}
if (path.Contains(':') || path.Contains('\\'))
{
return OsPathKind.Windows;
}
else if (path.Contains('/'))
{
return OsPathKind.Unix;
}
else
{
return OsPathKind.Unknown;
}
}
private static String FixSlashes(String path, OsPathKind kind)
{
if (kind == OsPathKind.Windows)
{
return path.Replace('/', '\\');
}
else if (kind == OsPathKind.Unix)
{
return path.Replace('\\', '/');
}
return path;
}
public OsPathKind Kind
{
get { return _kind; }
}
public Boolean IsWindowsPath
{
get { return _kind == OsPathKind.Windows; }
}
public Boolean IsUnixPath
{
get { return _kind == OsPathKind.Unix; }
}
public Boolean IsEmpty
{
get
{
return _path.IsNullOrWhiteSpace();
}
}
public Boolean IsRooted
{
get
{
if (IsWindowsPath)
{
return _path.StartsWith(@"\\") || _path.Contains(':');
}
else if (IsUnixPath)
{
return _path.StartsWith("/");
}
else
{
return false;
}
}
}
public OsPath Directory
{
get
{
var index = GetFileNameIndex();
if (index == -1)
{
return new OsPath(null);
}
else
{
return new OsPath(_path.Substring(0, index), _kind).AsDirectory();
}
}
}
public String FullPath
{
get
{
return _path;
}
}
public String FileName
{
get
{
var index = GetFileNameIndex();
if (index == -1)
{
var path = _path.Trim('\\', '/');
if (path.Length == 0)
{
return null;
}
return path;
}
else
{
return _path.Substring(index).Trim('\\', '/');
}
}
}
private Int32 GetFileNameIndex()
{
if (_path.Length < 2)
{
return -1;
}
var index = _path.LastIndexOfAny(new[] { '/', '\\' }, _path.Length - 2);
if (index == -1)
{
return -1;
}
if (_path.StartsWith(@"\\") && index < 2)
{
return -1;
}
if (_path.StartsWith("/") && index == 0)
{
index++;
}
return index;
}
private String[] GetFragments()
{
return _path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
}
public override String ToString()
{
return _path;
}
public override Int32 GetHashCode()
{
return _path.ToLowerInvariant().GetHashCode();
}
public override Boolean Equals(Object obj)
{
if (obj is OsPath)
{
return Equals((OsPath)obj);
}
else if (obj is String)
{
return Equals(new OsPath(obj as String));
}
else
{
return false;
}
}
public OsPath AsDirectory()
{
if (IsEmpty)
{
return this;
}
if (Kind == OsPathKind.Windows)
{
return new OsPath(_path.TrimEnd('\\') + "\\", _kind);
}
else if (Kind == OsPathKind.Unix)
{
return new OsPath(_path.TrimEnd('/') + "/", _kind);
}
else
{
return this;
}
}
public Boolean Contains(OsPath other)
{
if (!IsRooted || !other.IsRooted)
{
return false;
}
var leftFragments = GetFragments();
var rightFragments = other.GetFragments();
if (rightFragments.Length < leftFragments.Length)
{
return false;
}
var stringComparison = (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
for (int i = 0; i < leftFragments.Length; i++)
{
if (!String.Equals(leftFragments[i], rightFragments[i], stringComparison))
{
return false;
}
}
return true;
}
public Boolean Equals(OsPath other)
{
if (ReferenceEquals(other, null)) return false;
if (_path == other._path)
{
return true;
}
var left = _path;
var right = other._path;
if (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows)
{
return String.Equals(left, right, StringComparison.InvariantCultureIgnoreCase);
}
else
{
return String.Equals(left, right, StringComparison.InvariantCulture);
}
}
public static Boolean operator ==(OsPath left, OsPath right)
{
if (ReferenceEquals(left, null)) return ReferenceEquals(right, null);
return left.Equals(right);
}
public static Boolean operator !=(OsPath left, OsPath right)
{
if (ReferenceEquals(left, null)) return !ReferenceEquals(right, null);
return !left.Equals(right);
}
public static OsPath operator +(OsPath left, OsPath right)
{
if (left.Kind != right.Kind && right.Kind != OsPathKind.Unknown)
{
throw new Exception(String.Format("Cannot combine OsPaths of different platforms ('{0}' + '{1}')", left, right));
}
if (right.IsEmpty)
{
return left;
}
if (right.IsRooted)
{
return right;
}
if (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows)
{
return new OsPath(String.Join("\\", left._path.TrimEnd('\\'), right._path.TrimStart('\\')), OsPathKind.Windows);
}
else if (left.Kind == OsPathKind.Unix || right.Kind == OsPathKind.Unix)
{
return new OsPath(String.Join("/", left._path.TrimEnd('/'), right._path), OsPathKind.Unix);
}
else
{
return new OsPath(String.Join("/", left._path, right._path), OsPathKind.Unknown);
}
}
public static OsPath operator +(OsPath left, String right)
{
return left + new OsPath(right);
}
public static OsPath operator -(OsPath left, OsPath right)
{
if (!left.IsRooted || !right.IsRooted)
{
throw new ArgumentException("Cannot determine relative path for unrooted paths.");
}
var leftFragments = left.GetFragments();
var rightFragments = right.GetFragments();
var stringComparison = (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
int i;
for (i = 0; i < leftFragments.Length && i < rightFragments.Length; i++)
{
if (!String.Equals(leftFragments[i], rightFragments[i], stringComparison))
{
break;
}
}
if (i == 0)
{
return right;
}
var newFragments = new List<String>();
for (int j = i; j < rightFragments.Length; j++)
{
newFragments.Add("..");
}
for (int j = i; j < leftFragments.Length; j++)
{
newFragments.Add(leftFragments[j]);
}
if (left.FullPath.EndsWith("\\") || left.FullPath.EndsWith("/"))
{
newFragments.Add(String.Empty);
}
if (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows)
{
return new OsPath(String.Join("\\", newFragments), OsPathKind.Unknown);
}
else
{
return new OsPath(String.Join("/", newFragments), OsPathKind.Unknown);
}
}
}
public enum OsPathKind
{
Unknown,
Windows,
Unix
}
}

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Common.Disk
{
[Flags]
public enum TransferMode
{
None = 0,
Move = 1,
Copy = 2,
HardLink = 4,
HardLinkOrCopy = Copy | HardLink
}
}

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Common.Http
{
public class JsonRpcRequestBuilder : HttpRequestBuilder
{
public String Method { get; private set; }
public List<Object> Parameters { get; private set; }
public JsonRpcRequestBuilder(String baseUri, String method, IEnumerable<Object> parameters)
: base (baseUri)
{
Method = method;
Parameters = parameters.ToList();
}
public override HttpRequest Build(String path)
{
var request = base.Build(path);
request.Method = HttpMethod.POST;
request.Headers.Accept = "application/json-rpc, application/json";
request.Headers.ContentType = "application/json-rpc";
var message = new Dictionary<String, Object>();
message["jsonrpc"] = "2.0";
message["method"] = Method;
message["params"] = Parameters;
message["id"] = CreateNextId();
request.Body = message.ToJson();
return request;
}
public String CreateNextId()
{
return Guid.NewGuid().ToString().Substring(0, 8);
}
}
}

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Common.Http
{
public class JsonRpcResponse<T>
{
public String Id { get; set; }
public T Result { get; set; }
public Object Error { get; set; }
}
}

@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using System.Linq;
using System.Text.RegularExpressions;
namespace NzbDrone.Common.Instrumentation
{
@ -7,18 +8,30 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[]
{
// Url
new Regex(@"(<=\?|&)apikey=(?<secret>\w+?)(?=\W|$|_)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(<=\?|&)[^=]*?(username|password)=(?<secret>\w+?)(?=\W|$|_)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&)(apikey|token|passkey|uid)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// NzbGet
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Sabnzbd
new Regex(@"""[^""]*(username|password|api_?key|nzb_key)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase)
};
new Regex(@"""email_(account|to|from|pwd)""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
//private static readonly Regex CleansingRegex = new Regex(@"(?<=apikey=)(\w+?)(?=\W|$|_)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
// uTorrent
new Regex(@"\[""[a-z._]*(|username|password)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\[""(boss_key|boss_key_salt|proxy\.proxy)"",\d,""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// BroadcastheNet
new Regex(@"""?method""?\s*:\s*""(getTorrents)"",\s*""?params""?\s*:\s*\[\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&)(authkey|torrent_pass)=(?<secret>[^&=]+?)(?=""|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
};
public static string Cleanse(string message)
{
@ -29,7 +42,16 @@ namespace NzbDrone.Common.Instrumentation
foreach (var regex in CleansingRules)
{
message = regex.Replace(message, m => m.Value.Replace(m.Groups["secret"].Index - m.Index, m.Groups["secret"].Length, "<removed>"));
message = regex.Replace(message, m =>
{
var value = m.Value;
foreach (var capture in m.Groups["secret"].Captures.OfType<Capture>().Reverse())
{
value = value.Replace(capture.Index - m.Index, capture.Length, "<removed>");
}
return value;
});
}
return message;

@ -71,8 +71,10 @@
<Compile Include="ConvertBase32.cs" />
<Compile Include="Crypto\HashProvider.cs" />
<Compile Include="DictionaryExtensions.cs" />
<Compile Include="Disk\OsPath.cs" />
<Compile Include="Disk\DiskProviderBase.cs" />
<Compile Include="Disk\IDiskProvider.cs" />
<Compile Include="Disk\TransferMode.cs" />
<Compile Include="EnsureThat\Ensure.cs" />
<Compile Include="EnsureThat\EnsureBoolExtensions.cs" />
<Compile Include="EnsureThat\EnsureCollectionExtensions.cs" />
@ -139,6 +141,8 @@
<Compile Include="Http\HttpProvider.cs" />
<Compile Include="Http\HttpRequest.cs" />
<Compile Include="Http\HttpResponse.cs" />
<Compile Include="Http\JsonRpcRequestBuilder.cs" />
<Compile Include="Http\JsonRpcResponse.cs" />
<Compile Include="Http\NzbDroneWebClient.cs">
<SubType>Component</SubType>
</Compile>

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Tv;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Parser.Model;

@ -3,6 +3,7 @@ using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
@ -13,14 +14,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
public class RetentionSpecificationFixture : CoreTest<RetentionSpecification>
{
private RemoteEpisode parseResult;
private RemoteEpisode _remoteEpisode;
[SetUp]
public void Setup()
{
parseResult = new RemoteEpisode
_remoteEpisode = new RemoteEpisode
{
Release = new ReleaseInfo()
Release = new ReleaseInfo() { DownloadProtocol = DownloadProtocol.Usenet }
};
}
@ -31,7 +32,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
private void WithAge(int days)
{
parseResult.Release.PublishDate = DateTime.Now.AddDays(-days);
_remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-days);
}
[Test]
@ -40,7 +41,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
WithRetention(0);
WithAge(100);
Subject.IsSatisfiedBy(parseResult, null).Accepted.Should().BeTrue();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
@ -49,7 +50,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
WithRetention(1000);
WithAge(100);
Subject.IsSatisfiedBy(parseResult, null).Accepted.Should().BeTrue();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
@ -58,7 +59,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
WithRetention(100);
WithAge(100);
Subject.IsSatisfiedBy(parseResult, null).Accepted.Should().BeTrue();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
@ -67,7 +68,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
WithRetention(10);
WithAge(100);
Subject.IsSatisfiedBy(parseResult, null).Accepted.Should().BeFalse();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
[Test]
@ -76,7 +77,18 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
WithRetention(0);
WithAge(100);
Subject.IsSatisfiedBy(parseResult, null).Accepted.Should().BeTrue();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_when_release_is_not_usenet()
{
_remoteEpisode.Release.DownloadProtocol = DownloadProtocol.Torrent;
WithRetention(10);
WithAge(100);
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
}
}

@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.Download
_completed = Builder<DownloadClientItem>.CreateListOfSize(1)
.All()
.With(h => h.Status = DownloadItemStatus.Completed)
.With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic())
.With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic()))
.With(h => h.Title = "Drone.S01E01.HDTV")
.Build()
.ToList();
@ -325,7 +325,7 @@ namespace NzbDrone.Core.Test.Download
_completed.AddRange(Builder<DownloadClientItem>.CreateListOfSize(2)
.All()
.With(h => h.Status = DownloadItemStatus.Completed)
.With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic())
.With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic()))
.With(h => h.Title = "Drone.S01E01.HDTV")
.Build());

@ -0,0 +1,135 @@
using System.IO;
using System.Net;
using System.Linq;
using Moq;
using NUnit.Framework;
using FluentAssertions;
using NzbDrone.Test.Common;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Clients.TorrentBlackhole;
using System;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
{
[TestFixture]
public class TorrentBlackholeFixture : DownloadClientFixtureBase<TorrentBlackhole>
{
protected String _completedDownloadFolder;
protected String _blackholeFolder;
protected String _filePath;
[SetUp]
public void Setup()
{
_completedDownloadFolder = @"c:\blackhole\completed".AsOsAgnostic();
_blackholeFolder = @"c:\blackhole\torrent".AsOsAgnostic();
_filePath = (@"c:\blackhole\torrent\" + _title + ".torrent").AsOsAgnostic();
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = new TorrentBlackholeSettings
{
TorrentFolder = _blackholeFolder,
WatchFolder = _completedDownloadFolder
};
}
protected void GivenFailedDownload()
{
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Throws(new WebException());
}
protected void GivenCompletedItem()
{
var targetDir = Path.Combine(_completedDownloadFolder, _title);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetDirectories(_completedDownloadFolder))
.Returns(new[] { targetDir });
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetFiles(targetDir, SearchOption.AllDirectories))
.Returns(new[] { Path.Combine(_completedDownloadFolder, "somefile.mkv") });
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetFileSize(It.IsAny<String>()))
.Returns(1000000);
}
[Test]
public void completed_download_should_have_required_properties()
{
GivenCompletedItem();
var result = Subject.GetItems().Single();
VerifyCompleted(result);
}
[Test]
public void should_return_category()
{
GivenCompletedItem();
var result = Subject.GetItems().Single();
// We must have a category or CDH won't pick it up.
result.Category.Should().NotBeNullOrWhiteSpace();
}
[Test]
public void Download_should_download_file_if_it_doesnt_exist()
{
var remoteEpisode = CreateRemoteEpisode();
Subject.Download(remoteEpisode);
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_downloadUrl, _filePath), Times.Once());
}
[Test]
public void Download_should_replace_illegal_characters_in_title()
{
var illegalTitle = "Saturday Night Live - S38E08 - Jeremy Renner/Maroon 5 [SDTV]";
var expectedFilename = Path.Combine(_blackholeFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV]" + Path.GetExtension(_filePath));
var remoteEpisode = CreateRemoteEpisode();
remoteEpisode.Release.Title = illegalTitle;
Subject.Download(remoteEpisode);
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), expectedFilename), Times.Once());
}
[Test]
public void GetItems_should_considered_locked_files_queued()
{
GivenCompletedItem();
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.IsFileLocked(It.IsAny<string>()))
.Returns(true);
var items = Subject.GetItems().ToList();
items.Count.Should().Be(1);
items.First().Status.Should().Be(DownloadItemStatus.Downloading);
}
[Test]
public void should_return_status_with_outputdirs()
{
var result = Subject.GetStatus();
result.IsLocalhost.Should().BeTrue();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(_completedDownloadFolder);
}
}
}

@ -46,6 +46,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
protected void GivenCompletedItem()
{
var targetDir = Path.Combine(_completedDownloadFolder, _title);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetDirectories(_completedDownloadFolder))
.Returns(new[] { targetDir });
@ -69,6 +70,17 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
VerifyCompleted(result);
}
[Test]
public void should_return_category()
{
GivenCompletedItem();
var result = Subject.GetItems().Single();
// We must have a category or CDH won't pick it up.
result.Category.Should().NotBeNullOrWhiteSpace();
}
[Test]
public void Download_should_download_file_if_it_doesnt_exist()
{

@ -0,0 +1,310 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Http;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.Deluge;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Test.Common;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
{
[TestFixture]
public class DelugeFixture : DownloadClientFixtureBase<Deluge>
{
protected DelugeTorrent _queued;
protected DelugeTorrent _downloading;
protected DelugeTorrent _failed;
protected DelugeTorrent _completed;
[SetUp]
public void Setup()
{
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = new DelugeSettings()
{
TvCategory = null
};
_queued = new DelugeTorrent
{
Hash = "HASH",
IsFinished = false,
State = DelugeTorrentStatus.Queued,
Name = _title,
Size = 1000,
BytesDownloaded = 0,
Progress = 0.0,
DownloadPath = "somepath"
};
_downloading = new DelugeTorrent
{
Hash = "HASH",
IsFinished = false,
State = DelugeTorrentStatus.Downloading,
Name = _title,
Size = 1000,
BytesDownloaded = 100,
Progress = 10.0,
DownloadPath = "somepath"
};
_failed = new DelugeTorrent
{
Hash = "HASH",
IsFinished = false,
State = DelugeTorrentStatus.Error,
Name = _title,
Size = 1000,
BytesDownloaded = 100,
Progress = 10.0,
Message = "Error",
DownloadPath = "somepath"
};
_completed = new DelugeTorrent
{
Hash = "HASH",
IsFinished = true,
State = DelugeTorrentStatus.Paused,
Name = _title,
Size = 1000,
BytesDownloaded = 1000,
Progress = 100.0,
DownloadPath = "somepath"
};
Mocker.GetMock<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
}
protected void GivenFailedDownload()
{
Mocker.GetMock<IDelugeProxy>()
.Setup(s => s.AddTorrentFromMagnet(It.IsAny<String>(), It.IsAny<DelugeSettings>()))
.Throws<InvalidOperationException>();
Mocker.GetMock<IDelugeProxy>()
.Setup(s => s.AddTorrentFromFile(It.IsAny<String>(), It.IsAny<Byte[]>(), It.IsAny<DelugeSettings>()))
.Throws<InvalidOperationException>();
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[1000]));
Mocker.GetMock<IDelugeProxy>()
.Setup(s => s.AddTorrentFromMagnet(It.IsAny<String>(), It.IsAny<DelugeSettings>()))
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951".ToLower())
.Callback(PrepareClientToReturnQueuedItem);
Mocker.GetMock<IDelugeProxy>()
.Setup(s => s.AddTorrentFromFile(It.IsAny<String>(), It.IsAny<Byte[]>(), It.IsAny<DelugeSettings>()))
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951".ToLower())
.Callback(PrepareClientToReturnQueuedItem);
}
protected virtual void GivenTorrents(List<DelugeTorrent> torrents)
{
if (torrents == null)
{
torrents = new List<DelugeTorrent>();
}
Mocker.GetMock<IDelugeProxy>()
.Setup(s => s.GetTorrents(It.IsAny<DelugeSettings>()))
.Returns(torrents.ToArray());
}
protected void PrepareClientToReturnQueuedItem()
{
GivenTorrents(new List<DelugeTorrent>
{
_queued
});
}
protected void PrepareClientToReturnDownloadingItem()
{
GivenTorrents(new List<DelugeTorrent>
{
_downloading
});
}
protected void PrepareClientToReturnFailedItem()
{
GivenTorrents(new List<DelugeTorrent>
{
_failed
});
}
protected void PrepareClientToReturnCompletedItem()
{
GivenTorrents(new List<DelugeTorrent>
{
_completed
});
}
[Test]
public void queued_item_should_have_required_properties()
{
PrepareClientToReturnQueuedItem();
var item = Subject.GetItems().Single();
VerifyQueued(item);
}
[Test]
public void downloading_item_should_have_required_properties()
{
PrepareClientToReturnDownloadingItem();
var item = Subject.GetItems().Single();
VerifyDownloading(item);
}
[Test]
public void failed_item_should_have_required_properties()
{
PrepareClientToReturnFailedItem();
var item = Subject.GetItems().Single();
VerifyFailed(item);
}
[Test]
public void completed_download_should_have_required_properties()
{
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
VerifyCompleted(item);
}
[Test]
public void Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(String magnetUrl, String expectedHash)
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
remoteEpisode.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteEpisode);
id.Should().Be(expectedHash);
}
[TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Paused)]
[TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading)]
[TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(DelugeTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
[TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Downloading)]
public void GetItems_should_return_queued_item_as_downloadItemStatus(String apiStatus, DownloadItemStatus expectedItemStatus)
{
_queued.State = apiStatus;
PrepareClientToReturnQueuedItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
}
[TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Paused)]
[TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading)]
[TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(DelugeTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
[TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Downloading)]
public void GetItems_should_return_downloading_item_as_downloadItemStatus(String apiStatus, DownloadItemStatus expectedItemStatus)
{
_downloading.State = apiStatus;
PrepareClientToReturnDownloadingItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
}
[TestCase(DelugeTorrentStatus.Paused, DownloadItemStatus.Completed, true)]
[TestCase(DelugeTorrentStatus.Checking, DownloadItemStatus.Downloading, true)]
[TestCase(DelugeTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
[TestCase(DelugeTorrentStatus.Seeding, DownloadItemStatus.Completed, true)]
public void GetItems_should_return_completed_item_as_downloadItemStatus(String apiStatus, DownloadItemStatus expectedItemStatus, Boolean expectedReadOnly)
{
_completed.State = apiStatus;
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
item.IsReadOnly.Should().Be(expectedReadOnly);
}
[Test]
public void GetItems_should_check_share_ratio_for_readonly()
{
_completed.State = DelugeTorrentStatus.Paused;
_completed.IsAutoManaged = true;
_completed.StopAtRatio = true;
_completed.StopRatio = 1.0;
_completed.Ratio = 1.01;
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(DownloadItemStatus.Completed);
item.IsReadOnly.Should().BeFalse();
}
[Test]
public void should_return_status_with_outputdirs()
{
var configItems = new Dictionary<String, Object>();
configItems.Add("download_location", @"C:\Downloads\Downloading\deluge".AsOsAgnostic());
configItems.Add("move_completed_path", @"C:\Downloads\Finished\deluge".AsOsAgnostic());
configItems.Add("move_completed", true);
Mocker.GetMock<IDelugeProxy>()
.Setup(v => v.GetConfig(It.IsAny<DelugeSettings>()))
.Returns(configItems);
var result = Subject.GetStatus();
result.IsLocalhost.Should().BeTrue();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\deluge".AsOsAgnostic());
}
}
}

@ -39,8 +39,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal(It.IsAny<String>(), It.IsAny<String>()))
.Returns<String, String>((h,r) => r);
.Setup(v => v.RemapRemoteToLocal(It.IsAny<String>(), It.IsAny<OsPath>()))
.Returns<String, OsPath>((h, r) => r);
}
protected virtual RemoteEpisode CreateRemoteEpisode()

@ -14,6 +14,7 @@ using NzbDrone.Core.Parser.Model;
using NzbDrone.Test.Common;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
{
@ -300,8 +301,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
public void should_return_status_with_mounted_outputdir()
{
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/tv"))
.Returns(@"O:\mymount".AsOsAgnostic());
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.Returns(new OsPath(@"O:\mymount".AsOsAgnostic()));
var result = Subject.GetStatus();
@ -314,8 +315,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
public void should_remap_storage_if_mounted()
{
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/tv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE"))
.Returns(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic());
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.Returns(new OsPath(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()));
GivenQueue(null);
GivenHistory(_completed);

@ -11,6 +11,7 @@ using NzbDrone.Core.Download.Clients.Sabnzbd.Responses;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
{
@ -303,15 +304,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
var result = Subject.GetItems().Single();
result.OutputPath.Should().Be((@"C:\sorted\" + title).AsOsAgnostic());
result.OutputPath.Should().Be(new OsPath((@"C:\sorted\" + title).AsOsAgnostic()).AsDirectory());
}
[Test]
public void should_remap_storage_if_mounted()
{
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/vv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE"))
.Returns(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic());
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.Returns(new OsPath(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()));
GivenQueue(null);
GivenHistory(_completed);
@ -370,8 +371,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
public void should_return_status_with_mounted_outputdir()
{
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/vv"))
.Returns(@"O:\mymount".AsOsAgnostic());
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.Returns(new OsPath(@"O:\mymount".AsOsAgnostic()));
GivenQueue(null);

@ -0,0 +1,363 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Http;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.Transmission;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Test.Common;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
{
[TestFixture]
public class TransmissionFixture : DownloadClientFixtureBase<Transmission>
{
protected TransmissionSettings _settings;
protected TransmissionTorrent _queued;
protected TransmissionTorrent _downloading;
protected TransmissionTorrent _failed;
protected TransmissionTorrent _completed;
[SetUp]
public void Setup()
{
_settings = new TransmissionSettings
{
Host = "127.0.0.1",
Port = 2222,
Username = "admin",
Password = "pass"
};
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = _settings;
_queued = new TransmissionTorrent
{
HashString = "HASH",
IsFinished = false,
Status = TransmissionTorrentStatus.Queued,
Name = _title,
TotalSize = 1000,
LeftUntilDone = 1000,
DownloadDir = "somepath"
};
_downloading = new TransmissionTorrent
{
HashString = "HASH",
IsFinished = false,
Status = TransmissionTorrentStatus.Downloading,
Name = _title,
TotalSize = 1000,
LeftUntilDone = 100,
DownloadDir = "somepath"
};
_failed = new TransmissionTorrent
{
HashString = "HASH",
IsFinished = false,
Status = TransmissionTorrentStatus.Stopped,
Name = _title,
TotalSize = 1000,
LeftUntilDone = 100,
ErrorString = "Error",
DownloadDir = "somepath"
};
_completed = new TransmissionTorrent
{
HashString = "HASH",
IsFinished = true,
Status = TransmissionTorrentStatus.Stopped,
Name = _title,
TotalSize = 1000,
LeftUntilDone = 0,
DownloadDir = "somepath"
};
Mocker.GetMock<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
var configItems = new Dictionary<String, Object>();
configItems.Add("download-dir", @"C:/Downloads/Finished/transmission");
configItems.Add("incomplete-dir", null);
configItems.Add("incomplete-dir-enabled", false);
Mocker.GetMock<ITransmissionProxy>()
.Setup(v => v.GetConfig(It.IsAny<TransmissionSettings>()))
.Returns(configItems);
}
protected void GivenTvCategory()
{
_settings.TvCategory = "nzbdrone";
}
protected void GivenFailedDownload()
{
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<TransmissionSettings>()))
.Throws<InvalidOperationException>();
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[1000]));
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<TransmissionSettings>()))
.Callback(PrepareClientToReturnQueuedItem);
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.AddTorrentFromData(It.IsAny<Byte[]>(), It.IsAny<String>(), It.IsAny<TransmissionSettings>()))
.Callback(PrepareClientToReturnQueuedItem);
}
protected virtual void GivenTorrents(List<TransmissionTorrent> torrents)
{
if (torrents == null)
{
torrents = new List<TransmissionTorrent>();
}
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.GetTorrents(It.IsAny<TransmissionSettings>()))
.Returns(torrents);
}
protected void PrepareClientToReturnQueuedItem()
{
GivenTorrents(new List<TransmissionTorrent>
{
_queued
});
}
protected void PrepareClientToReturnDownloadingItem()
{
GivenTorrents(new List<TransmissionTorrent>
{
_downloading
});
}
protected void PrepareClientToReturnFailedItem()
{
GivenTorrents(new List<TransmissionTorrent>
{
_failed
});
}
protected void PrepareClientToReturnCompletedItem()
{
GivenTorrents(new List<TransmissionTorrent>
{
_completed
});
}
[Test]
public void queued_item_should_have_required_properties()
{
PrepareClientToReturnQueuedItem();
var item = Subject.GetItems().Single();
VerifyQueued(item);
}
[Test]
public void downloading_item_should_have_required_properties()
{
PrepareClientToReturnDownloadingItem();
var item = Subject.GetItems().Single();
VerifyDownloading(item);
}
[Test]
public void failed_item_should_have_required_properties()
{
PrepareClientToReturnFailedItem();
var item = Subject.GetItems().Single();
VerifyFailed(item);
}
[Test]
public void completed_download_should_have_required_properties()
{
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
VerifyCompleted(item);
}
[Test]
public void Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
[Test]
public void Download_with_category_should_force_directory()
{
GivenTvCategory();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
Mocker.GetMock<ITransmissionProxy>()
.Verify(v => v.AddTorrentFromData(It.IsAny<Byte[]>(), @"C:/Downloads/Finished/transmission/.nzbdrone", It.IsAny<TransmissionSettings>()), Times.Once());
}
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(String magnetUrl, String expectedHash)
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
remoteEpisode.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteEpisode);
id.Should().Be(expectedHash);
}
[TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Downloading)]
[TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading)]
[TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading)]
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
[TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed)]
[TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)]
public void GetItems_should_return_queued_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
_queued.Status = apiStatus;
PrepareClientToReturnQueuedItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
}
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(TransmissionTorrentStatus.Downloading, DownloadItemStatus.Downloading)]
[TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed)]
public void GetItems_should_return_downloading_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
_downloading.Status = apiStatus;
PrepareClientToReturnDownloadingItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
}
[TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, false)]
[TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, true)]
[TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, true)]
[TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
[TestCase(TransmissionTorrentStatus.SeedingWait, DownloadItemStatus.Completed, true)]
[TestCase(TransmissionTorrentStatus.Seeding, DownloadItemStatus.Completed, true)]
public void GetItems_should_return_completed_item_as_downloadItemStatus(TransmissionTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, Boolean expectedReadOnly)
{
_completed.Status = apiStatus;
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
item.IsReadOnly.Should().Be(expectedReadOnly);
}
[Test]
public void should_return_status_with_outputdirs()
{
var result = Subject.GetStatus();
result.IsLocalhost.Should().BeTrue();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\transmission");
}
[Test]
public void should_exclude_items_not_in_category()
{
GivenTvCategory();
_downloading.DownloadDir = @"C:/Downloads/Finished/transmission/.nzbdrone";
GivenTorrents(new List<TransmissionTorrent>
{
_downloading,
_queued
});
var items = Subject.GetItems().ToList();
items.Count.Should().Be(1);
items.First().Status.Should().Be(DownloadItemStatus.Downloading);
}
[Test]
public void should_fix_forward_slashes()
{
WindowsOnly();
_downloading.DownloadDir = @"C:/Downloads/Finished/transmission";
GivenTorrents(new List<TransmissionTorrent>
{
_downloading
});
var items = Subject.GetItems().ToList();
items.Should().HaveCount(1);
items.First().OutputPath.Should().Be(@"C:\Downloads\Finished\transmission\" + _title);
}
[TestCase("2.84 ()")]
[TestCase("2.84+ ()")]
[TestCase("2.84 (other info)")]
[TestCase("2.84 (2.84)")]
public void should_version_should_only_check_version_number(String version)
{
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.GetVersion(It.IsAny<TransmissionSettings>()))
.Returns(version);
Subject.Test();
}
}
}

@ -0,0 +1,340 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Http;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.UTorrent;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Test.Common;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
{
[TestFixture]
public class UTorrentFixture : DownloadClientFixtureBase<UTorrent>
{
protected UTorrentTorrent _queued;
protected UTorrentTorrent _downloading;
protected UTorrentTorrent _failed;
protected UTorrentTorrent _completed;
[SetUp]
public void Setup()
{
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = new UTorrentSettings
{
Host = "127.0.0.1",
Port = 2222,
Username = "admin",
Password = "pass",
TvCategory = "tv"
};
_queued = new UTorrentTorrent
{
Hash = "HASH",
Status = UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Loaded,
Name = _title,
Size = 1000,
Remaining = 1000,
Progress = 0,
Label = "tv",
DownloadUrl = _downloadUrl,
RootDownloadPath = "somepath"
};
_downloading = new UTorrentTorrent
{
Hash = "HASH",
Status = UTorrentTorrentStatus.Started | UTorrentTorrentStatus.Loaded,
Name = _title,
Size = 1000,
Remaining = 100,
Progress = 0.9,
Label = "tv",
DownloadUrl = _downloadUrl,
RootDownloadPath = "somepath"
};
_failed = new UTorrentTorrent
{
Hash = "HASH",
Status = UTorrentTorrentStatus.Error,
Name = _title,
Size = 1000,
Remaining = 100,
Progress = 0.9,
Label = "tv",
DownloadUrl = _downloadUrl,
RootDownloadPath = "somepath"
};
_completed = new UTorrentTorrent
{
Hash = "HASH",
Status = UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Loaded,
Name = _title,
Size = 1000,
Remaining = 0,
Progress = 1.0,
Label = "tv",
DownloadUrl = _downloadUrl,
RootDownloadPath = "somepath"
};
Mocker.GetMock<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
}
protected void GivenRedirectToMagnet()
{
var httpHeader = new HttpHeader();
httpHeader["Location"] = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp";
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, httpHeader, new Byte[0], System.Net.HttpStatusCode.SeeOther));
}
protected void GivenFailedDownload()
{
Mocker.GetMock<IUTorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<UTorrentSettings>()))
.Throws<InvalidOperationException>();
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IUTorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<String>(), It.IsAny<UTorrentSettings>()))
.Callback(() =>
{
PrepareClientToReturnQueuedItem();
});
}
protected virtual void GivenTorrents(List<UTorrentTorrent> torrents)
{
if (torrents == null)
{
torrents = new List<UTorrentTorrent>();
}
Mocker.GetMock<IUTorrentProxy>()
.Setup(s => s.GetTorrents(It.IsAny<UTorrentSettings>()))
.Returns(torrents);
}
protected void PrepareClientToReturnQueuedItem()
{
GivenTorrents(new List<UTorrentTorrent>
{
_queued
});
}
protected void PrepareClientToReturnDownloadingItem()
{
GivenTorrents(new List<UTorrentTorrent>
{
_downloading
});
}
protected void PrepareClientToReturnFailedItem()
{
GivenTorrents(new List<UTorrentTorrent>
{
_failed
});
}
protected void PrepareClientToReturnCompletedItem()
{
GivenTorrents(new List<UTorrentTorrent>
{
_completed
});
}
[Test]
public void queued_item_should_have_required_properties()
{
PrepareClientToReturnQueuedItem();
var item = Subject.GetItems().Single();
VerifyQueued(item);
}
[Test]
public void downloading_item_should_have_required_properties()
{
PrepareClientToReturnDownloadingItem();
var item = Subject.GetItems().Single();
VerifyDownloading(item);
}
[Test]
public void failed_item_should_have_required_properties()
{
PrepareClientToReturnFailedItem();
var item = Subject.GetItems().Single();
VerifyFailed(item);
}
[Test]
public void completed_download_should_have_required_properties()
{
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
VerifyCompleted(item);
}
[Test]
public void Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
[Test]
public void GetItems_should_ignore_downloads_from_other_categories()
{
_completed.Label = "myowncat";
PrepareClientToReturnCompletedItem();
var items = Subject.GetItems();
items.Should().BeEmpty();
}
// Proxy.GetTorrents does not return original url. So item has to be found via magnet url.
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(String magnetUrl, String expectedHash)
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
remoteEpisode.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteEpisode);
id.Should().Be(expectedHash);
}
[TestCase(UTorrentTorrentStatus.Loaded, DownloadItemStatus.Queued)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Started, DownloadItemStatus.Downloading)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Started, DownloadItemStatus.Downloading)]
public void GetItems_should_return_queued_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
_queued.Status = apiStatus;
PrepareClientToReturnQueuedItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
}
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued, DownloadItemStatus.Queued)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Started, DownloadItemStatus.Downloading)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Started, DownloadItemStatus.Downloading)]
public void GetItems_should_return_downloading_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus)
{
_downloading.Status = apiStatus;
PrepareClientToReturnDownloadingItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
}
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checking, DownloadItemStatus.Queued, false)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked, DownloadItemStatus.Completed, false)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued, DownloadItemStatus.Completed, true)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Started, DownloadItemStatus.Completed, true)]
[TestCase(UTorrentTorrentStatus.Loaded | UTorrentTorrentStatus.Checked | UTorrentTorrentStatus.Queued | UTorrentTorrentStatus.Paused, DownloadItemStatus.Completed, true)]
public void GetItems_should_return_completed_item_as_downloadItemStatus(UTorrentTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, Boolean expectedReadOnly)
{
_completed.Status = apiStatus;
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
item.Status.Should().Be(expectedItemStatus);
item.IsReadOnly.Should().Be(expectedReadOnly);
}
[Test]
public void should_return_status_with_outputdirs()
{
var configItems = new Dictionary<String, String>();
configItems.Add("dir_active_download_flag", "true");
configItems.Add("dir_active_download", @"C:\Downloads\Downloading\utorrent".AsOsAgnostic());
configItems.Add("dir_completed_download", @"C:\Downloads\Finished\utorrent".AsOsAgnostic());
configItems.Add("dir_completed_download_flag", "true");
configItems.Add("dir_add_label", "true");
Mocker.GetMock<IUTorrentProxy>()
.Setup(v => v.GetConfig(It.IsAny<UTorrentSettings>()))
.Returns(configItems);
var result = Subject.GetStatus();
result.IsLocalhost.Should().BeTrue();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\utorrent\tv".AsOsAgnostic());
}
[Test]
public void should_combine_drive_letter()
{
WindowsOnly();
_completed.RootDownloadPath = "D:";
PrepareClientToReturnCompletedItem();
var item = Subject.GetItems().Single();
item.OutputPath.Should().Be(@"D:\" + _title);
}
[Test]
public void Download_should_handle_http_redirect_to_magnet()
{
GivenRedirectToMagnet();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
}
}

@ -0,0 +1,61 @@
{
"id":"9787693d",
"result":{
"torrents":{
"123":{
"GroupName":"2014.09.15",
"GroupID":"237457",
"TorrentID":"123",
"SeriesID":"1034",
"Series":"Jimmy Kimmel Live",
"SeriesBanner":"https:\/\/cdn2.broadcasthe.net\/tvdb\/banners\/graphical\/71998-g.jpg",
"SeriesPoster":"https:\/\/cdn2.broadcasthe.net\/tvdb\/banners\/posters\/71998-3.jpg",
"YoutubeTrailer":"http:\/\/www.youtube.com\/v\/w3NwB9PLxss",
"Category":"Episode",
"Snatched":"40",
"Seeders":"40",
"Leechers":"9",
"Source":"HDTV",
"Container":"MP4",
"Codec":"x264",
"Resolution":"SD",
"Origin":"Scene",
"ReleaseName":"Jimmy.Kimmel.2014.09.15.Jane.Fonda.HDTV.x264-aAF",
"Size":"505099926",
"Time":"1410902133",
"TvdbID":"71998",
"TvrageID":"4055",
"ImdbID":"0320037",
"InfoHash":"123",
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=123&torrent_pass=123"
},
"1234":{
"GroupName":"S01E02",
"GroupID":"237456",
"TorrentID":"1234",
"SeriesID":"45853",
"Series":"Mammon",
"SeriesBanner":"https:\/\/cdn2.broadcasthe.net\/tvdb\/banners\/text\/274366.jpg",
"SeriesPoster":"\/\/cdn2.broadcasthe.net\/tvdb\/banners\/posters\/274366-2.jpg",
"YoutubeTrailer":"http:\/\/www.youtube.com\/v\/1VVbJecvHr8",
"Category":"Episode",
"Snatched":"0",
"Seeders":"1",
"Leechers":"23",
"Source":"HDTV",
"Container":"TS",
"Codec":"h.264",
"Resolution":"1080i",
"Origin":"Internal",
"ReleaseName":"Mammon.S01E02.1080i.HDTV.H.264-Irishman",
"Size":"4021238596",
"Time":"1410901918",
"TvdbID":"274366",
"TvrageID":"38472",
"ImdbID":"2377081",
"InfoHash":"1234",
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=1234&authkey=1234&torrent_pass=1234"
}},
"results":"117927"
}
}

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<rss version="0.91">
<channel>
<ttl>10</ttl>
<title>BitMeTV.ORG</title>
<link>http://www.bitmetv.org</link>
<description>This is a private - by registration only - website. You can help keep it alive by donating: http://www.bitmetv.org/donate.php</description>
<language>en-usde</language>
<copyright>Copyright © 2004 - 2007 BitMeTV.ORG</copyright>
<webMaster>noreply@bitmetv.org</webMaster>
<image>
<title>BitMeTV.ORG</title>
<url>http://www.bitmetv.org/favicon.ico</url>
<link>http://www.bitmetv.org</link>
<width>16</width>
<height>16</height>
<description>This is a private - by registration only - website. You can help keep it alive by donating: http://www.bitmetv.org/donate.php</description>
</image>
<item>
<title>Total.Divas.S02E08.HDTV.x264-CRiMSON</title>
<link>http://www.bitmetv.org/download.php/12/Total.Divas.S02E08.HDTV.x264-CRiMSON.torrent</link>
<pubDate>Tue, 13 May 2014 17:04:29 -0000</pubDate>
<description>
Category: (Reality TV - Un-scripted)
Size: 376.71 MB
</description>
</item>
<item>
<title>Aqua.Teen.Hunger.Force.S10.INTERNAL.HDTV.x264-BitMeTV</title>
<link>http://www.bitmetv.org/download.php/34/Aqua.Teen.Hunger.Force.S10.INTERNAL.HDTV.x264-BitMeTV.torrent</link>
<pubDate>Tue, 13 May 2014 17:03:12 -0000</pubDate>
<description>
Category: (Adult Swim)
Size: 725.46 MB
</description>
</item>
<item>
<title>Antiques.Roadshow.US.S18E16.720p.HDTV.x264-BAJSKORV</title>
<link>http://www.bitmetv.org/download.php/56/Antiques.Roadshow.US.S18E16.720p.HDTV.x264-BAJSKORV.torrent</link>
<pubDate>Tue, 13 May 2014 16:47:05 -0000</pubDate>
<description>
Category: (Reality TV - Un-scripted)
Size: 960.15 MB
</description>
</item>
<item>
<title>Seth.Meyers.2014.05.12.Chris.O.Dowd-Emma.Roberts.HDTV.x264-CROOKS</title>
<link>http://www.bitmetv.org/download.php/78/Seth.Meyers.2014.05.12.Chris.O.Dowd-Emma.Roberts.HDTV.x264-CROOKS.torrent</link>
<pubDate>Tue, 13 May 2014 16:01:21 -0000</pubDate>
<description>
Category: Seth Meyers
Size: 301.31 MB
</description>
</item>
<item>
<title>The.Mole.Australia.Season.4</title>
<link>http://www.bitmetv.org/download.php/910/The%20Mole%20Australia%20-%20Season%204.torrent</link>
<pubDate>Tue, 13 May 2014 15:52:54 -0000</pubDate>
<description>
Category: (Reality TV - Competitive)
Size: 2.13 GB
</description>
</item>
</channel>
</rss>

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE torrent PUBLIC "-//bitTorrent//DTD torrent 0.1//EN" "http://xmlns.ezrss.it/0.1/dtd/">
<rss version="2.0">
<channel>
<title>ezRSS - Latest torrent releases</title>
<ttl>15</ttl>
<link>http://ezrss.it/feed/</link>
<image>
<title>ezRSS - Latest torrent releases</title>
<url>http://ezrss.it/images/ezrssit.png</url>
<link>http://ezrss.it/feed/</link>
</image>
<description>The latest 30 torrent releases.</description>
<item>
<title><![CDATA[S4C I Grombil Cyfandir Pell American Interior [PDTV - MVGROUP]]]></title>
<link>http://re.zoink.it/20a4ed4eFC</link>
<category domain="http://eztv.it/shows/187/mv-group-documentaries/"><![CDATA[TV Show / MV Group Documentaries]]></category>
<pubDate>Mon, 15 Sep 2014 13:39:00 -0500</pubDate>
<description><![CDATA[Show Name: S4C I Grombil Cyfandir Pell American Interior; Episode Title: N/A; Episode Date: ]]></description>
<enclosure url="http://re.zoink.it/20a4ed4eFC" length="796606175" type="application/x-bittorrent" />
<comments>http://eztv.it/forum/discuss/58439/</comments>
<guid>http://eztv.it/ep/58439/s4c-i-grombil-cyfandir-pell-american-interior-pdtv-x264-mvgroup/</guid>
<torrent xmlns="http://xmlns.ezrss.it/0.1/">
<fileName><![CDATA[S4C.I.Grombil.Cyfandir.Pell.American.Interior.PDTV.x264-MVGroup.[MVGroup.org].torrent]]></fileName>
<contentLength>796606175</contentLength>
<infoHash>20FC4FBFA88272274AC671F857CC15144E9AA83E</infoHash>
<magnetURI><![CDATA[magnet:?xt=urn:btih:ED6E7P5IQJZCOSWGOH4FPTAVCRHJVKB6&dn=S4C.I.Grombil.Cyfandir.Pell.American.Interior.PDTV.x264-MVGroup]]></magnetURI>
</torrent>
</item>
<item>
<title><![CDATA[Andy McNabs Tour Of Duty Series 1 - Courage Under Fire 1x6 [DVDRIP - MVGROUP]]]></title>
<link>http://re.zoink.it/AAa65f6eA2</link>
<category domain="http://eztv.it/shows/187/mv-group-documentaries/"><![CDATA[TV Show / MV Group Documentaries]]></category>
<pubDate>Mon, 15 Sep 2014 13:04:21 -0500</pubDate>
<description><![CDATA[Show Name: Andy McNabs Tour Of Duty Series 1; Episode Title: Courage Under Fire; Season: 1; Episode: 6]]></description>
<enclosure url="http://re.zoink.it/AAa65f6eA2" length="698999946" type="application/x-bittorrent" />
<comments>http://eztv.it/forum/discuss/58438/</comments>
<guid>http://eztv.it/ep/58438/andy-mcnabs-tour-of-duty-series-1-6of6-courage-under-fire-dvdrip-x264-mvgroup/</guid>
<torrent xmlns="http://xmlns.ezrss.it/0.1/">
<fileName><![CDATA[Andy.McNabs.Tour.Of.Duty.Series.1.6of6.Courage.Under.Fire.DVDRip.x264-MVGroup.[MVGroup.org].torrent]]></fileName>
<contentLength>698999946</contentLength>
<infoHash>AAA2038BED9EBCA2C312D1C9C3E8E024D0EB414E</infoHash>
<magnetURI><![CDATA[magnet:?xt=urn:btih:VKRAHC7NT26KFQYS2HE4H2HAETIOWQKO&dn=Andy.McNabs.Tour.Of.Duty.Series.1.6of6.Courage.Under.Fire.DVDRip.x264-MVGroup]]></magnetURI>
</torrent>
</item>
<item>
<title><![CDATA[So You Think You Can Drive [HDTV - MVGROUP]]]></title>
<link>http://re.zoink.it/54a65da3D5</link>
<category domain="http://eztv.it/shows/187/mv-group-documentaries/"><![CDATA[TV Show / MV Group Documentaries]]></category>
<pubDate>Mon, 15 Sep 2014 09:19:32 -0500</pubDate>
<description><![CDATA[Show Name: So You Think You Can Drive; Episode Title: N/A; Episode Date: ]]></description>
<enclosure url="http://re.zoink.it/54a65da3D5" length="1163302273" type="application/x-bittorrent" />
<comments>http://eztv.it/forum/discuss/58437/</comments>
<guid>http://eztv.it/ep/58437/so-you-think-you-can-drive-x264-hdtv-mvgroup/</guid>
<torrent xmlns="http://xmlns.ezrss.it/0.1/">
<fileName><![CDATA[So.You.Think.You.Can.Drive.x264.HDTV-MVGroup.[MVGroup.org].torrent]]></fileName>
<contentLength>1163302273</contentLength>
<infoHash>54D50B8352B2C54A1A3AD952269A56D2D95A3DF4</infoHash>
<magnetURI><![CDATA[magnet:?xt=urn:btih:KTKQXA2SWLCUUGR23FJCNGSW2LMVUPPU&dn=So.You.Think.You.Can.Drive.x264.HDTV-MVGroup]]></magnetURI>
</torrent>
</item>
</channel>
</rss>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0">
<channel>
<item>
<title>24 S03E12 720p WEBRip h264-DRAWER</title>
<link>http://iptorrents.com/download.php/1234/24.S03E12.720p.WEBRip.h264-DRAWER.torrent?torrent_pass=abcd</link>
<pubDate>Mon, 12 May 2014 19:06:34 +0000</pubDate>
<description>Category: TV/x264 Size: 1.37 GB </description>
</item>
<item>
<title>Rosemary&#39;s Baby S01E01 Part 1 1080p WEB-DL DD5 1 H 264-BS</title>
<link>http://iptorrents.com/download.php/1234/Rosemary&#39;s.Baby.S01E01.Part.1.1080p.WEB-DL.DD5.1.H.264-BS.torrent?torrent_pass=abcd</link>
<pubDate>Mon, 12 May 2014 19:06:25 +0000</pubDate>
<description>556 MB; TV/x264</description>
</item>
<item>
<title>Rosemary&#39;s Baby S01E01 Part 1 720p WEB-DL DD5 1 H 264-BS</title>
<link>http://iptorrents.com/download.php/1234/Rosemary&#39;s.Baby.S01E01.Part.1.720p.WEB-DL.DD5.1.H.264-BS.torrent?torrent_pass=abcd</link>
<pubDate>Mon, 12 May 2014 19:04:09 +0000</pubDate>
<description>Category: TV/x264 Size: 2.65 GB </description>
</item>
<item>
<title>24 S03E11 720p WEBRip h264-DRAWER</title>
<link>http://iptorrents.com/download.php/1234/24.S03E11.720p.WEBRip.h264-DRAWER.torrent?torrent_pass=abcd</link>
<pubDate>Mon, 12 May 2014 19:02:54 +0000</pubDate>
<description>Category: TV/x264 Size: 1.33 GB </description>
</item>
<item>
<title>Da Vincis Demons S02E08 1080p WEB-DL DD5 1 H 264-BS</title>
<link>http://iptorrents.com/download.php/1234/Da.Vincis.Demons.S02E08.1080p.WEB-DL.DD5.1.H.264-BS.torrent?torrent_pass=abcd</link>
<pubDate>Mon, 12 May 2014 19:02:11 +0000</pubDate>
<description>Category: TV/x264 Size: 1.92 GB </description>
</item>
</channel>
</rss>

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:torrent="http://xmlns.ezrss.it/0.1/">
<channel>
<title>tv torrents RSS feed - KickassTorrents</title>
<link>http://kickass.to/</link>
<description>tv torrents RSS feed</description>
<item>
<title>Doctor Stranger.E03.140512.HDTV.H264.720p-iPOP.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/doctor-stranger-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100648.html</link>
<guid>http://kickass.to/doctor-stranger-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100648.html</guid>
<pubDate>Mon, 12 May 2014 16:16:49 +0000</pubDate>
<torrent:contentLength>1205364736</torrent:contentLength>
<torrent:infoHash>208C4F7866612CC88BFEBC7C496FA72C2368D1C0</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:208C4F7866612CC88BFEBC7C496FA72C2368D1C0&dn=doctor+stranger+e03+140512+hdtv+h264+720p+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>206</torrent:seeds>
<torrent:peers>311</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>doctor.stranger.e03.140512.hdtv.h264.720p.ipop.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/208C4F7866612CC88BFEBC7C496FA72C2368D1C0.torrent?title=[kickass.to]doctor.stranger.e03.140512.hdtv.h264.720p.ipop.avi.ctrg" length="1205364736" type="application/x-bittorrent" />
</item>
<item>
<title>Triangle.E03.140512.HDTV.XViD-iPOP.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/triangle-e03-140512-hdtv-xvid-ipop-avi-ctrg-t9100647.html</link>
<guid>http://kickass.to/triangle-e03-140512-hdtv-xvid-ipop-avi-ctrg-t9100647.html</guid>
<pubDate>Mon, 12 May 2014 16:16:31 +0000</pubDate>
<torrent:contentLength>677543936</torrent:contentLength>
<torrent:infoHash>BF22A53C9889A7D325F2A3D904E566B7DF4074EB</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:BF22A53C9889A7D325F2A3D904E566B7DF4074EB&dn=triangle+e03+140512+hdtv+xvid+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>242</torrent:seeds>
<torrent:peers>374</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>triangle.e03.140512.hdtv.xvid.ipop.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/BF22A53C9889A7D325F2A3D904E566B7DF4074EB.torrent?title=[kickass.to]triangle.e03.140512.hdtv.xvid.ipop.avi.ctrg" length="677543936" type="application/x-bittorrent" />
</item>
<item>
<title>Triangle.E03.140512.HDTV.H264.720p-iPOP.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/triangle-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100646.html</link>
<guid>http://kickass.to/triangle-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100646.html</guid>
<pubDate>Mon, 12 May 2014 16:16:10 +0000</pubDate>
<torrent:contentLength>1196869632</torrent:contentLength>
<torrent:infoHash>8427BFB8884B8228364EBB9B3EA7D8B77E03A7BC</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:8427BFB8884B8228364EBB9B3EA7D8B77E03A7BC&dn=triangle+e03+140512+hdtv+h264+720p+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>177</torrent:seeds>
<torrent:peers>268</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>triangle.e03.140512.hdtv.h264.720p.ipop.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/8427BFB8884B8228364EBB9B3EA7D8B77E03A7BC.torrent?title=[kickass.to]triangle.e03.140512.hdtv.h264.720p.ipop.avi.ctrg" length="1196869632" type="application/x-bittorrent" />
</item>
<item>
<title>Triangle.E03.140512.HDTV.X264.720p-BarosG_.avi [CTRG]</title>
<category>TV</category>
<author>http://kickass.to/user/2NE1/</author>
<link>http://kickass.to/triangle-e03-140512-hdtv-x264-720p-barosg-avi-ctrg-t9100644.html</link>
<guid>http://kickass.to/triangle-e03-140512-hdtv-x264-720p-barosg-avi-ctrg-t9100644.html</guid>
<pubDate>Mon, 12 May 2014 16:15:52 +0000</pubDate>
<torrent:contentLength>1418906266</torrent:contentLength>
<torrent:infoHash>5556B773893DB55287ECEC581E850B853163DB11</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:5556B773893DB55287ECEC581E850B853163DB11&dn=triangle+e03+140512+hdtv+x264+720p+barosg+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>522</torrent:seeds>
<torrent:peers>785</torrent:peers>
<torrent:verified>1</torrent:verified>
<torrent:fileName>triangle.e03.140512.hdtv.x264.720p.barosg.avi.ctrg.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/5556B773893DB55287ECEC581E850B853163DB11.torrent?title=[kickass.to]triangle.e03.140512.hdtv.x264.720p.barosg.avi.ctrg" length="1418906266" type="application/x-bittorrent" />
</item>
<item>
<title>Battlestar Galactica 1978 Dvd3 e09 e10 e11 e12 [NL] [FR] [ENG] Sub</title>
<description>
<![CDATA[In een afgelegen zonnestelsel leeft een mensenras op twaalf koloniewerelden. Ze zijn al eeuwen in oorlog met de Cylons, gevechtsrobots die ooit werden gemaakt door een allang verdwenen buitenaards reptielachtig ras. Met de hulp van de menselijke verrader Baltar zijn de Cylons erin geslaagd de mensheid vrijwel uit te roeien. Slechts een oorlogsschip kan aan de vernietiging ontkomen: de Battlestar Galactica van commandant Adama.
Met een vloot burgerschepen vol vluchtelingen vlucht de Galactica voor de Cylons. Adama besluit op zoek te gaan naar de legendarische 13e en laatste kolonie, genaamd Aarde. Tijdens de lange en gevaarlijke reis worden ze voortdurend bedreigd door de achtervolgende Cylons en andere gevaren.]]>
</description>
<category>TV</category>
<author>http://kickass.to/user/hendriknl/</author>
<link>http://kickass.to/battlestar-galactica-1978-dvd3-e09-e10-e11-e12-nl-fr-eng-sub-t9100642.html</link>
<guid>http://kickass.to/battlestar-galactica-1978-dvd3-e09-e10-e11-e12-nl-fr-eng-sub-t9100642.html</guid>
<pubDate>Mon, 12 May 2014 16:15:46 +0000</pubDate>
<torrent:contentLength>4680841216</torrent:contentLength>
<torrent:infoHash>3D293CAFEDAC595F6E55F9C284DD76862FE254F6</torrent:infoHash>
<torrent:magnetURI><![CDATA[magnet:?xt=urn:btih:3D293CAFEDAC595F6E55F9C284DD76862FE254F6&dn=battlestar+galactica+1978+dvd3+e09+e10+e11+e12+nl+fr+eng+sub&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce]]></torrent:magnetURI>
<torrent:seeds>2</torrent:seeds>
<torrent:peers>5</torrent:peers>
<torrent:verified>0</torrent:verified>
<torrent:fileName>battlestar.galactica.1978.dvd3.e09.e10.e11.e12.nl.fr.eng.sub.torrent</torrent:fileName>
<enclosure url="http://torcache.net/torrent/3D293CAFEDAC595F6E55F9C284DD76862FE254F6.torrent?title=[kickass.to]battlestar.galactica.1978.dvd3.e09.e10.e11.e12.nl.fr.eng.sub" length="4680841216" type="application/x-bittorrent" />
</item>
</channel>
</rss>

@ -0,0 +1,40 @@
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>NyaaTorrents</title>
<link>http://www.nyaa.se/</link>
<atom:link href="http://www.nyaa.se/?page=rss" rel="self" type="application/rss+xml" />
<description></description>
<item>
<title>[TSRaws] Futsuu no Joshikousei ga [Locodol] Yattemita. #07 &#40;TBS&#41;.ts</title>
<category>Raw Anime</category>
<link>http://www.nyaa.se/?page=download&#38;tid=587750</link>
<guid>http://www.nyaa.se/?page=view&#38;tid=587750</guid>
<description><![CDATA[1 seeder(s), 2 leecher(s), 0 download(s) - 2.35 GiB]]></description>
<pubDate>Thu, 14 Aug 2014 18:10:36 +0000</pubDate>
</item>
<item>
<title>[JIGGYSUB] KOI KOI 7 EP07 [R2DVD 420P H264 AC3]</title>
<category>English-translated Anime</category>
<link>http://www.nyaa.se/?page=download&#38;tid=587749</link>
<guid>http://www.nyaa.se/?page=view&#38;tid=587749</guid>
<description><![CDATA[1 seeder(s), 2 leecher(s), 25 download(s) - 1.36 GiB]]></description>
<pubDate>Thu, 14 Aug 2014 18:05:22 +0000</pubDate>
</item>
<item>
<title>[Ohys-Raws] RAIL WARS! - 07 &#40;TBS 1280x720 x264 AAC&#41;.mp4</title>
<category>Raw Anime</category>
<link>http://www.nyaa.se/?page=download&#38;tid=587748</link>
<guid>http://www.nyaa.se/?page=view&#38;tid=587748</guid>
<description><![CDATA[2 seeder(s), 111 leecher(s), 243 download(s) - 424.2 MiB]]></description>
<pubDate>Thu, 14 Aug 2014 18:02:57 +0000</pubDate>
</item>
<item>
<title>[Arabasma.com] Naruto Shippuuden - 372 [Arabic Sub] [MQ].mp4</title>
<category>Non-English-translated Anime</category>
<link>http://www.nyaa.se/?page=download&#38;tid=587747</link>
<guid>http://www.nyaa.se/?page=view&#38;tid=587747</guid>
<description><![CDATA[1 seeder(s), 0 leecher(s), 23 download(s) - 69.5 MiB]]></description>
<pubDate>Thu, 14 Aug 2014 18:01:36 +0000</pubDate>
</item>
</channel>
</rss>

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>TorrentLeech</title>
<link>http://www.torrentleech.org</link>
<description>The latest torrents from TorrentLeech.org</description>
<language>en</language>
<ttl>5</ttl>
<atom:link href="http://rss.torrentleech.org/4fd6a70f990234472f40" rel="self" type="application/rss+xml" />
<item>
<title><![CDATA[Classic Car Rescue S02E04 720p HDTV x264-C4TV]]></title>
<pubDate>Mon, 12 May 2014 19:15:28 +0000</pubDate>
<category>Episodes HD</category>
<guid>http://www.torrentleech.org/torrent/513575</guid>
<comments><![CDATA[http://www.torrentleech.org/torrent/513575#comments]]></comments>
<link><![CDATA[http://www.torrentleech.org/rss/download/513575/1234/Classic.Car.Rescue.S02E04.720p.HDTV.x264-C4TV.torrent]]></link>
<description><![CDATA[Category: Episodes HD - Seeders: 1 - Leechers: 7]]></description>
</item>
<item>
<title><![CDATA[24 S03E14 720p WEBRip h264-DRAWER]]></title>
<pubDate>Mon, 12 May 2014 19:14:09 +0000</pubDate>
<category>Episodes HD</category>
<guid>http://www.torrentleech.org/torrent/513574</guid>
<comments><![CDATA[http://www.torrentleech.org/torrent/513574#comments]]></comments>
<link><![CDATA[http://www.torrentleech.org/rss/download/513574/1234/24.S03E14.720p.WEBRip.h264-DRAWER.torrent]]></link>
<description><![CDATA[Category: Episodes HD - Seeders: 13 - Leechers: 11]]></description>
</item>
<item>
<title><![CDATA[24 S03E13 720p WEBRip h264-DRAWER]]></title>
<pubDate>Mon, 12 May 2014 19:09:18 +0000</pubDate>
<category>Episodes HD</category>
<guid>http://www.torrentleech.org/torrent/513573</guid>
<comments><![CDATA[http://www.torrentleech.org/torrent/513573#comments]]></comments>
<link><![CDATA[http://www.torrentleech.org/rss/download/513573/1234/24.S03E13.720p.WEBRip.h264-DRAWER.torrent]]></link>
<description><![CDATA[Category: Episodes HD - Seeders: 19 - Leechers: 7]]></description>
</item>
<item>
<title><![CDATA[24 S03E11 720p WEBRip h264-DRAWER]]></title>
<pubDate>Mon, 12 May 2014 19:09:10 +0000</pubDate>
<category>Episodes HD</category>
<guid>http://www.torrentleech.org/torrent/513572</guid>
<comments><![CDATA[http://www.torrentleech.org/torrent/513572#comments]]></comments>
<link><![CDATA[http://www.torrentleech.org/rss/download/513572/1234/24.S03E11.720p.WEBRip.h264-DRAWER.torrent]]></link>
<description><![CDATA[Category: Episodes HD - Seeders: 19 - Leechers: 7]]></description>
</item>
<item>
<title><![CDATA[Meet Joe Black 1998 1080p HDDVD x264-FSiHD]]></title>
<pubDate>Mon, 12 May 2014 19:06:59 +0000</pubDate>
<category>HD</category>
<guid>http://www.torrentleech.org/torrent/513571</guid>
<comments><![CDATA[http://www.torrentleech.org/torrent/513571#comments]]></comments>
<link><![CDATA[http://www.torrentleech.org/rss/download/513571/1234/Meet.Joe.Black.1998.1080p.HDDVD.x264-FSiHD.torrent]]></link>
<description><![CDATA[Category: HD - Seeders: 1 - Leechers: 10]]></description>
</item>
</channel>
</rss>

@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.With(v => v.State == TrackedDownloadState.Downloading)
.With(v => v.DownloadItem = new DownloadClientItem())
.With(v => v.DownloadItem.Status = DownloadItemStatus.Completed)
.With(v => v.DownloadItem.OutputPath = @"C:\Test\DropFolder\myfile.mkv".AsOsAgnostic())
.With(v => v.DownloadItem.OutputPath = new OsPath(@"C:\Test\DropFolder\myfile.mkv".AsOsAgnostic()))
.Build();
Mocker.GetMock<IDownloadTrackingService>()
@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
GivenCompletedDownloadHandling(true);
GivenDroneFactoryFolder(true);
_completed.First().DownloadItem.OutputPath = (DRONE_FACTORY_FOLDER + @"\myfile.mkv").AsOsAgnostic();
_completed.First().DownloadItem.OutputPath = new OsPath((DRONE_FACTORY_FOLDER + @"\myfile.mkv").AsOsAgnostic());
Subject.Check().ShouldBeWarning();
}

@ -0,0 +1,58 @@
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.BitMeTv;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using System;
using System.Linq;
using FluentAssertions;
namespace NzbDrone.Core.Test.IndexerTests.BitMeTvTests
{
[TestFixture]
public class BitMeTvFixture : CoreTest<BitMeTv>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "BitMeTV",
Settings = new BitMeTvSettings()
};
}
[Test]
public void should_parse_recent_feed_from_BitMeTv()
{
var recentFeed = ReadAllText(@"Files/RSS/BitMeTv.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Total.Divas.S02E08.HDTV.x264-CRiMSON");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("http://www.bitmetv.org/download.php/12/Total.Divas.S02E08.HDTV.x264-CRiMSON.torrent");
torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/05/13 17:04:29"));
torrentInfo.Size.Should().Be(395009065);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(null);
torrentInfo.Seeds.Should().Be(null);
}
}
}

@ -0,0 +1,137 @@
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.BroadcastheNet;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Test.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using FluentAssertions;
using NzbDrone.Core.Indexers.Exceptions;
namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
{
[TestFixture]
public class BroadcastheNetFixture : CoreTest<BroadcastheNet>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "BroadcastheNet",
Settings = new BroadcastheNetSettings() { ApiKey = "abc" }
};
}
[Test]
public void should_parse_recent_feed_from_BroadcastheNet()
{
var recentFeed = ReadAllText(@"Files/Indexers/BroadcastheNet/RecentFeed.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(2);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Guid.Should().Be("BTN-123");
torrentInfo.Title.Should().Be("Jimmy.Kimmel.2014.09.15.Jane.Fonda.HDTV.x264-aAF");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://broadcasthe.net/torrents.php?action=download&id=123&authkey=123&torrent_pass=123");
torrentInfo.InfoUrl.Should().Be("https://broadcasthe.net/torrents.php?id=237457&torrentid=123");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/09/16 21:15:33"));
torrentInfo.Size.Should().Be(505099926);
torrentInfo.InfoHash.Should().Be("123");
torrentInfo.TvRageId.Should().Be(4055);
torrentInfo.MagnetUrl.Should().BeNullOrEmpty();
torrentInfo.Peers.Should().Be(9);
torrentInfo.Seeds.Should().Be(40);
}
private void VerifyBackOff()
{
// TODO How to detect (and implement) back-off logic.
}
[Test]
public void should_back_off_on_bad_request()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0], System.Net.HttpStatusCode.BadRequest));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_back_off_and_report_api_key_invalid()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0], System.Net.HttpStatusCode.Unauthorized));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_back_off_on_unknown_method()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0], System.Net.HttpStatusCode.NotFound));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_back_off_api_limit_reached()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0], System.Net.HttpStatusCode.ServiceUnavailable));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
}
}

@ -0,0 +1,63 @@
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Eztv;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Test.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using FluentAssertions;
namespace NzbDrone.Core.Test.IndexerTests.EztvTests
{
[TestFixture]
public class EztvFixture : CoreTest<Eztv>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Eztv",
Settings = new EztvSettings()
};
}
[Test]
public void should_parse_recent_feed_from_Eztv()
{
var recentFeed = ReadAllText(@"Files/RSS/Eztv.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(3);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("S4C I Grombil Cyfandir Pell American Interior [PDTV - MVGROUP]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("http://re.zoink.it/20a4ed4eFC");
torrentInfo.InfoUrl.Should().Be("http://eztv.it/ep/58439/s4c-i-grombil-cyfandir-pell-american-interior-pdtv-x264-mvgroup/");
torrentInfo.CommentUrl.Should().Be("http://eztv.it/forum/discuss/58439/");
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/09/15 18:39:00"));
torrentInfo.Size.Should().Be(796606175);
torrentInfo.InfoHash.Should().Be("20FC4FBFA88272274AC671F857CC15144E9AA83E");
torrentInfo.MagnetUrl.Should().Be("magnet:?xt=urn:btih:ED6E7P5IQJZCOSWGOH4FPTAVCRHJVKB6&dn=S4C.I.Grombil.Cyfandir.Pell.American.Interior.PDTV.x264-MVGroup");
torrentInfo.Peers.Should().NotHaveValue();
torrentInfo.Seeds.Should().NotHaveValue();
}
}
}

@ -0,0 +1,63 @@
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.IPTorrents;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Test.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using FluentAssertions;
namespace NzbDrone.Core.Test.IndexerTests.IPTorrentsTests
{
[TestFixture]
public class IPTorrentsFixture : CoreTest<IPTorrents>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "IPTorrents",
Settings = new IPTorrentsSettings() { Url = "http://fake.com/" }
};
}
[Test]
public void should_parse_recent_feed_from_IPTorrents()
{
var recentFeed = ReadAllText(@"Files/RSS/IPTorrents.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("24 S03E12 720p WEBRip h264-DRAWER");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("http://iptorrents.com/download.php/1234/24.S03E12.720p.WEBRip.h264-DRAWER.torrent?torrent_pass=abcd");
torrentInfo.InfoUrl.Should().BeNullOrEmpty();
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/05/12 19:06:34"));
torrentInfo.Size.Should().Be(1471026299);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(null);
torrentInfo.Seeds.Should().Be(null);
}
}
}

@ -1,69 +1,211 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Eztv;
using NzbDrone.Core.Indexers.Fanzub;
using NzbDrone.Core.Indexers.KickassTorrents;
using NzbDrone.Core.Indexers.Newznab;
using NzbDrone.Core.Indexers.Nyaa;
using NzbDrone.Core.Indexers.Wombles;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NUnit.Framework;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Test.Common.Categories;
using System.Linq;
namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests
{
[IntegrationTest]
public class IndexerIntegrationTests : CoreTest<Wombles>
public class IndexerIntegrationTests : CoreTest
{
private SingleEpisodeSearchCriteria _singleSearchCriteria;
private AnimeEpisodeSearchCriteria _animeSearchCriteria;
[SetUp]
public void SetUp()
{
UseRealHttp();
_singleSearchCriteria = new SingleEpisodeSearchCriteria()
{
SceneTitles = new List<string> { "Person of Interest" },
SeasonNumber = 1,
EpisodeNumber = 1
};
_animeSearchCriteria = new AnimeEpisodeSearchCriteria()
{
SceneTitles = new List<string> { "Steins;Gate" },
AbsoluteEpisodeNumber = 1
};
}
[Test]
public void wombles_fetch_recent()
{
var indexer = Mocker.Resolve<Wombles>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = NullConfig.Instance
};
var result = indexer.FetchRecent();
ValidateResult(result);
}
[Test]
public void fanzub_fetch_recent()
{
Assert.Inconclusive("Fanzub Down");
var indexer = Mocker.Resolve<Fanzub>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = NullConfig.Instance
};
var result = indexer.FetchRecent();
ValidateResult(result);
}
[Test]
public void wombles_rss()
public void fanzub_search_single()
{
Subject.Definition = new IndexerDefinition
Assert.Inconclusive("Fanzub Down");
var indexer = Mocker.Resolve<Fanzub>();
indexer.Definition = new IndexerDefinition
{
Name = "Wombles",
Name = "MyIndexer",
Settings = NullConfig.Instance
};
var result = Subject.FetchRecent();
var result = indexer.Fetch(_animeSearchCriteria);
ValidateResult(result);
}
[Test]
public void kickass_fetch_recent()
{
var indexer = Mocker.Resolve<KickassTorrents>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = new KickassTorrentsSettings()
};
var result = indexer.FetchRecent();
ValidateTorrentResult(result, hasSize: true);
}
[Test]
public void kickass_search_single()
{
var indexer = Mocker.Resolve<KickassTorrents>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = new KickassTorrentsSettings()
};
var result = indexer.Fetch(_singleSearchCriteria);
ValidateTorrentResult(result, hasSize: true, hasMagnet: true);
}
[Test]
public void eztv_fetch_recent()
{
var indexer = Mocker.Resolve<Eztv>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = new EztvSettings()
};
var result = indexer.FetchRecent();
ValidateTorrentResult(result, hasSize: true, hasMagnet: true);
}
[Test]
public void nyaa_fetch_recent()
{
var indexer = Mocker.Resolve<Nyaa>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = new NyaaSettings()
};
var result = indexer.FetchRecent();
ValidateTorrentResult(result, hasSize: true);
}
[Test]
public void nyaa_search_single()
{
var indexer = Mocker.Resolve<Nyaa>();
indexer.Definition = new IndexerDefinition
{
Name = "MyIndexer",
Settings = new NyaaSettings()
};
var result = indexer.Fetch(_animeSearchCriteria);
ValidateResult(result, skipSize: true, skipInfo: true);
ValidateTorrentResult(result, hasSize: true);
}
private void ValidateResult(IList<ReleaseInfo> reports, bool skipSize = false, bool skipInfo = false)
private void ValidateResult(IList<ReleaseInfo> reports, bool hasSize = false, bool hasInfoUrl = false)
{
reports.Should().NotBeEmpty();
reports.Should().NotContain(c => string.IsNullOrWhiteSpace(c.Title));
reports.Should().NotContain(c => string.IsNullOrWhiteSpace(c.DownloadUrl));
reports.Should().OnlyContain(c => c.Title.IsNotNullOrWhiteSpace());
reports.Should().OnlyContain(c => c.PublishDate.Year > 2000);
reports.Should().OnlyContain(c => c.DownloadUrl.IsNotNullOrWhiteSpace());
reports.Should().OnlyContain(c => c.DownloadUrl.StartsWith("http"));
if (!skipInfo)
if (hasInfoUrl)
{
reports.Should().NotContain(c => string.IsNullOrWhiteSpace(c.InfoUrl));
reports.Should().OnlyContain(c => c.InfoUrl.IsNotNullOrWhiteSpace());
}
if (!skipSize)
if (hasSize)
{
reports.Should().OnlyContain(c => c.Size > 0);
}
}
private void ValidateTorrentResult(IList<ReleaseInfo> reports, bool skipSize = false, bool skipInfo = false)
private void ValidateTorrentResult(IList<ReleaseInfo> reports, bool hasSize = false, bool hasInfoUrl = false, bool hasMagnet = false)
{
reports.Should().OnlyContain(c => c.GetType() == typeof(TorrentInfo));
ValidateResult(reports, skipSize, skipInfo);
ValidateResult(reports, hasSize, hasInfoUrl);
reports.Should().OnlyContain(c => c.DownloadUrl.EndsWith(".torrent"));
reports.Should().OnlyContain(c => c.DownloadProtocol == DownloadProtocol.Torrent);
reports.Cast<TorrentInfo>().Should().OnlyContain(c => c.MagnetUrl.StartsWith("magnet:"));
reports.Cast<TorrentInfo>().Should().NotContain(c => string.IsNullOrWhiteSpace(c.InfoHash));
if (hasMagnet)
{
reports.Cast<TorrentInfo>().Should().OnlyContain(c => c.MagnetUrl.StartsWith("magnet:"));
}
}
}

@ -0,0 +1,93 @@
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.KickassTorrents;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Test.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using FluentAssertions;
namespace NzbDrone.Core.Test.IndexerTests.KickassTorrentsTests
{
[TestFixture]
public class KickassTorrentsFixture : CoreTest<KickassTorrents>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Kickass Torrents",
Settings = new KickassTorrentsSettings() { VerifiedOnly = false }
};
}
[Test]
public void should_parse_recent_feed_from_KickassTorrents()
{
var recentFeed = ReadAllText(@"Files/RSS/KickassTorrents.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Doctor Stranger.E03.140512.HDTV.H264.720p-iPOP.avi [CTRG]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("http://torcache.net/torrent/208C4F7866612CC88BFEBC7C496FA72C2368D1C0.torrent?title=[kickass.to]doctor.stranger.e03.140512.hdtv.h264.720p.ipop.avi.ctrg");
torrentInfo.InfoUrl.Should().Be("http://kickass.to/doctor-stranger-e03-140512-hdtv-h264-720p-ipop-avi-ctrg-t9100648.html");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/05/12 16:16:49"));
torrentInfo.Size.Should().Be(1205364736);
torrentInfo.InfoHash.Should().Be("208C4F7866612CC88BFEBC7C496FA72C2368D1C0");
torrentInfo.MagnetUrl.Should().Be("magnet:?xt=urn:btih:208C4F7866612CC88BFEBC7C496FA72C2368D1C0&dn=doctor+stranger+e03+140512+hdtv+h264+720p+ipop+avi+ctrg&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce");
torrentInfo.Peers.Should().Be(311);
torrentInfo.Seeds.Should().Be(206);
}
[Test]
public void should_return_empty_list_on_404()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0], System.Net.HttpStatusCode.NotFound));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(0);
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_not_return_unverified_releases_if_not_configured()
{
(Subject.Definition.Settings as KickassTorrentsSettings).VerifiedOnly = true;
var recentFeed = ReadAllText(@"Files/RSS/KickassTorrents.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(4);
}
}
}

@ -0,0 +1,57 @@
using System;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Nyaa;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
{
[TestFixture]
public class NyaaFixture : CoreTest<Nyaa>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Nyaa",
Settings = new NyaaSettings()
};
}
[Test]
public void should_parse_recent_feed_from_Nyaa()
{
var recentFeed = ReadAllText(@"Files/RSS/Nyaa.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(4);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("[TSRaws] Futsuu no Joshikousei ga [Locodol] Yattemita. #07 (TBS).ts");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("http://www.nyaa.se/?page=download&tid=587750");
torrentInfo.InfoUrl.Should().Be("http://www.nyaa.se/?page=view&tid=587750");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/08/14 18:10:36"));
torrentInfo.Size.Should().Be(2523293286); //2.35 GiB
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(2);
torrentInfo.Seeds.Should().Be(1);
}
}
}

@ -0,0 +1,63 @@
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Torrentleech;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Test.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using FluentAssertions;
namespace NzbDrone.Core.Test.IndexerTests.TorrentleechTests
{
[TestFixture]
public class TorrentleechFixture : CoreTest<Torrentleech>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "Torrentleech",
Settings = new TorrentleechSettings()
};
}
[Test]
public void should_parse_recent_feed_from_Torrentleech()
{
var recentFeed = ReadAllText(@"Files/RSS/Torrentleech.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(5);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("Classic Car Rescue S02E04 720p HDTV x264-C4TV");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("http://www.torrentleech.org/rss/download/513575/1234/Classic.Car.Rescue.S02E04.720p.HDTV.x264-C4TV.torrent");
torrentInfo.InfoUrl.Should().Be("http://www.torrentleech.org/torrent/513575");
torrentInfo.CommentUrl.Should().Be("http://www.torrentleech.org/torrent/513575#comments");
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/05/12 19:15:28"));
torrentInfo.Size.Should().Be(0);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(7);
torrentInfo.Seeds.Should().Be(1);
}
}
}

@ -140,11 +140,15 @@
<Compile Include="DecisionEngineTests\UpgradeDiskSpecificationFixture.cs" />
<Compile Include="Download\CompletedDownloadServiceFixture.cs" />
<Compile Include="Download\DownloadApprovedReportsTests\DownloadApprovedFixture.cs" />
<Compile Include="Download\DownloadClientTests\Blackhole\TorrentBlackholeFixture.cs" />
<Compile Include="Download\DownloadClientTests\Blackhole\UsenetBlackholeFixture.cs" />
<Compile Include="Download\DownloadClientTests\DelugeTests\DelugeFixture.cs" />
<Compile Include="Download\DownloadClientTests\DownloadClientFixtureBase.cs" />
<Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" />
<Compile Include="Download\DownloadClientTests\PneumaticProviderFixture.cs" />
<Compile Include="Download\DownloadClientTests\SabnzbdTests\SabnzbdFixture.cs" />
<Compile Include="Download\DownloadClientTests\TransmissionTests\TransmissionFixture.cs" />
<Compile Include="Download\DownloadClientTests\UTorrentTests\UTorrentFixture.cs" />
<Compile Include="Download\DownloadServiceFixture.cs" />
<Compile Include="Download\FailedDownloadServiceFixture.cs" />
<Compile Include="Download\Pending\PendingReleaseServiceTests\RemoveRejectedFixture.cs" />
@ -180,6 +184,9 @@
<Compile Include="IndexerSearchTests\SearchDefinitionFixture.cs" />
<Compile Include="IndexerTests\AnimezbTests\AnimezbFixture.cs" />
<Compile Include="IndexerTests\BasicRssParserFixture.cs" />
<Compile Include="IndexerTests\BitMeTvTests\BitMeTvFixture.cs" />
<Compile Include="IndexerTests\BroadcastheNetTests\BroadcastheNetFixture.cs" />
<Compile Include="IndexerTests\EztvTests\EztvFixture.cs" />
<Compile Include="IndexerTests\IndexerServiceFixture.cs" />
<Compile Include="IndexerTests\IntegrationTests\IndexerIntegrationTests.cs" />
<Compile Include="IndexerTests\NewznabTests\NewznabFixture.cs" />
@ -190,6 +197,10 @@
<Compile Include="IndexerTests\SeasonSearchFixture.cs" />
<Compile Include="IndexerTests\TestIndexer.cs" />
<Compile Include="IndexerTests\TestIndexerSettings.cs" />
<Compile Include="IndexerTests\IPTorrentsTests\IPTorrentsFixture.cs" />
<Compile Include="IndexerTests\KickassTorrentsTests\KickassTorrentsFixture.cs" />
<Compile Include="IndexerTests\NyaaTests\NyaaFixture.cs" />
<Compile Include="IndexerTests\TorrentleechTests\TorrentleechFixture.cs" />
<Compile Include="IndexerTests\XElementExtensionsFixture.cs" />
<Compile Include="InstrumentationTests\DatabaseTargetFixture.cs" />
<Compile Include="JobTests\JobRepositoryFixture.cs" />
@ -328,9 +339,16 @@
<Link>sqlite3.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="Files\Indexers\BroadcastheNet\RecentFeed.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Content Include="Files\RSS\fanzub.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\RSS\Eztv.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="License.txt" />
<None Include="..\NzbDrone.Test.Common\App.config">
<Link>App.config</Link>
</None>
@ -359,6 +377,9 @@
<Content Include="Files\QueueUnknownPriority.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\RSS\Nyaa.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\RSS\filesharingtalk.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@ -396,6 +417,18 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<SubType>Designer</SubType>
</Content>
<Content Include="Files\RSS\BitMeTv.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\RSS\IPTorrents.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\RSS\KickassTorrents.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\RSS\Torrentleech.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Include="Files\TestArchive.tar.gz">

@ -36,6 +36,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Hawaii Five 0", "hawaiifive0")]
[TestCase("Match of the Day", "matchday")]
[TestCase("Match of the Day 2", "matchday2")]
[TestCase("[ www.Torrenting.com ] - Revenge.S03E14.720p.HDTV.X264-DIMENSION", "Revenge")]
[TestCase("Seed S02E09 HDTV x264-2HD [eztv]-[rarbg.com]", "Seed")]
public void should_parse_series_name(String postTitle, String title)
{
var result = Parser.Parser.ParseSeriesName(postTitle);
@ -50,6 +52,12 @@ namespace NzbDrone.Core.Test.ParserTests
title.CleanSeriesTitle().Should().Be("carnivale");
}
[TestCase("Discovery TV - Gold Rush : 02 Road From Hell [S04].mp4")]
public void should_clean_up_invalid_path_characters(String postTitle)
{
Parser.Parser.ParseTitle(postTitle);
}
[TestCase("[scnzbefnet][509103] 2.Broke.Girls.S03E18.720p.HDTV.X264-DIMENSION", "2 Broke Girls")]
public void should_remove_request_info_from_title(String postTitle, String title)
{

@ -23,6 +23,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Real Time with Bill Maher S12E17 May 23, 2014.mp4", null)]
[TestCase("Reizen Waes - S01E08 - Transistrië, Zuid-Ossetië en Abchazië SDTV.avi", null)]
[TestCase("Simpsons 10x11 - Wild Barts Cant Be Broken [rl].avi", null)]
[TestCase("[ www.Torrenting.com ] - Revenge.S03E14.720p.HDTV.X264-DIMENSION", "DIMENSION")]
[TestCase("Seed S02E09 HDTV x264-2HD [eztv]-[rarbg.com]", "2HD")]
public void should_parse_release_group(string title, string expected)
{
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);

@ -21,7 +21,6 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Parenthood.2010.S02E14.HDTV.XviD-LOL", "Parenthood 2010", 2, 14)]
[TestCase("Hawaii Five 0 S01E19 720p WEB DL DD5 1 H 264 NT", "Hawaii Five 0", 1, 19)]
[TestCase("The Event S01E14 A Message Back 720p WEB DL DD5 1 H264 SURFER", "The Event", 1, 14)]
[TestCase("Constantine S1-E1-WEB-DL-1080p-NZBgeek", "Constantine", 1, 1)]
[TestCase("Adam Hills In Gordon St Tonight S01E07 WS PDTV XviD FUtV", "Adam Hills In Gordon St Tonight", 1, 7)]
[TestCase("Adventure.Inc.S03E19.DVDRip.XviD-OSiTV", "Adventure.Inc", 3, 19)]
[TestCase("S03E09 WS PDTV XviD FUtV", "", 3, 9)]
@ -92,6 +91,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Constantine S1-E1-WEB-DL-1080p-NZBgeek", "Constantine", 1, 1)]
[TestCase("Constantine S1E1-WEB-DL-1080p-NZBgeek", "Constantine", 1, 1)]
[TestCase("NCIS.S010E16.720p.HDTV.X264-DIMENSION", "NCIS", 10, 16)]
[TestCase("[ www.Torrenting.com ] - Revolution.2012.S02E17.720p.HDTV.X264-DIMENSION", "Revolution2012", 2, 17)]
[TestCase("Revolution.2012.S02E18.720p.HDTV.X264-DIMENSION.mkv", "Revolution2012", 2, 18)]
//[TestCase("", "", 0, 0)]
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
{

@ -26,6 +26,10 @@ namespace NzbDrone.Core.Test.RemotePathMappingsTests
Mocker.GetMock<IRemotePathMappingRepository>()
.Setup(s => s.All())
.Returns(new List<RemotePathMapping>());
Mocker.GetMock<IRemotePathMappingRepository>()
.Setup(s => s.Insert(It.IsAny<RemotePathMapping>()))
.Returns<RemotePathMapping>(m => m);
}
private void GivenMapping()
@ -93,13 +97,13 @@ namespace NzbDrone.Core.Test.RemotePathMappingsTests
GivenMapping();
var result = Subject.RemapRemoteToLocal(host, remotePath);
var result = Subject.RemapRemoteToLocal(host, new OsPath(remotePath));
result.Should().Be(expectedLocalPath);
}
[TestCase("my-server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")]
[TestCase("my-server.localdomain", "/mnt/storage", @"D:\mountedstorage")]
[TestCase("my-server.localdomain", "/mnt/storage/", @"D:\mountedstorage")]
[TestCase("my-2server.localdomain", "/mnt/storage/downloads/tv", "/mnt/storage/downloads/tv")]
[TestCase("my-server.localdomain", "/mnt/storageabc/downloads/tv", "/mnt/storageabc/downloads/tv")]
public void should_remap_local_to_remote(String host, String expectedRemotePath, String localPath)
@ -108,9 +112,28 @@ namespace NzbDrone.Core.Test.RemotePathMappingsTests
GivenMapping();
var result = Subject.RemapLocalToRemote(host, localPath);
var result = Subject.RemapLocalToRemote(host, new OsPath(localPath));
result.Should().Be(expectedRemotePath);
}
[TestCase(@"\\server\share\with/mixed/slashes", @"\\server\share\with\mixed\slashes\")]
[TestCase(@"D:/with/forward/slashes", @"D:\with\forward\slashes\")]
[TestCase(@"D:/with/mixed\slashes", @"D:\with\mixed\slashes\")]
public void should_fix_wrong_slashes_on_add(String remotePath, String cleanedPath)
{
GivenMapping();
var mapping = new RemotePathMapping
{
Host = "my-server.localdomain",
RemotePath = remotePath,
LocalPath = @"D:\mountedstorage\downloads\tv" .AsOsAgnostic()
};
var result = Subject.Add(mapping);
result.RemotePath.Should().Be(cleanedPath);
}
}
}

@ -212,6 +212,13 @@ namespace NzbDrone.Core.Configuration
set { SetValue("SkipFreeSpaceCheckWhenImporting", value); }
}
public Boolean CopyUsingHardlinks
{
get { return GetValueBoolean("CopyUsingHardlinks", true); }
set { SetValue("CopyUsingHardlinks", value); }
}
public Boolean SetPermissionsLinux
{
get { return GetValueBoolean("SetPermissionsLinux", false); }

@ -36,6 +36,7 @@ namespace NzbDrone.Core.Configuration
Boolean CreateEmptySeriesFolders { get; set; }
FileDateType FileDate { get; set; }
Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
Boolean CopyUsingHardlinks { get; set; }
//Permissions (Media Management)
Boolean SetPermissionsLinux { get; set; }

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Marr.Data.Converters;
using Marr.Data.Mapping;
using Newtonsoft.Json;
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Datastore.Converters
{
public class OsPathConverter : IConverter
{
public Object FromDB(ConverterContext context)
{
if (context.DbValue == DBNull.Value)
{
return DBNull.Value;
}
var value = (String)context.DbValue;
return new OsPath(value);
}
public Object FromDB(ColumnMap map, Object dbValue)
{
return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue });
}
public Object ToDB(Object clrValue)
{
var value = (OsPath)clrValue;
return value.FullPath;
}
public Type DbType
{
get { return typeof(String); }
}
}
}

@ -28,6 +28,7 @@ using NzbDrone.Core.SeriesStats;
using NzbDrone.Core.Tags;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Datastore
{
@ -114,6 +115,7 @@ namespace NzbDrone.Core.Datastore
MapRepository.Instance.RegisterTypeConverter(typeof(ParsedEpisodeInfo), new EmbeddedDocumentConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<Int32>), new EmbeddedDocumentConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter());
}
private static void RegisterProviderSettingConverter()

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.DecisionEngine
@ -19,7 +20,9 @@ namespace NzbDrone.Core.DecisionEngine
.GroupBy(c => c.RemoteEpisode.Series.Id, (i, s) => s
.OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(s.First().RemoteEpisode.Series.Profile))
.ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault())
.ThenBy(c => c.RemoteEpisode.Release.DownloadProtocol)
.ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / Math.Max(1, c.RemoteEpisode.Episodes.Count))
.ThenByDescending(c => TorrentInfo.GetSeeders(c.RemoteEpisode.Release))
.ThenBy(c => c.RemoteEpisode.Release.Age))
.SelectMany(c => c)
.Union(decisions.Where(c => c.RemoteEpisode.Series == null))

@ -20,6 +20,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
{
if (subject.Release.DownloadProtocol != Indexers.DownloadProtocol.Usenet)
{
_logger.Debug("Not checking retention requirement for non-usenet report");
return Decision.Accept();
}
var age = subject.Release.Age;
var retention = _configService.Retention;

@ -0,0 +1,43 @@
using NLog;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
{
public class TorrentSeedingSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;
public TorrentSeedingSpecification(Logger logger)
{
_logger = logger;
}
public RejectionType Type
{
get
{
return RejectionType.Permanent;
}
}
public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
{
var torrentInfo = remoteEpisode.Release as TorrentInfo;
if (torrentInfo == null)
{
return Decision.Accept();
}
if (torrentInfo.Seeds != null && torrentInfo.Seeds < 1)
{
_logger.Debug("Not enough seeders. ({0})", torrentInfo.Seeds);
return Decision.Reject("Not enough seeders. ({0})", torrentInfo.Seeds);
}
return Decision.Accept();
}
}
}

@ -0,0 +1,303 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Validation;
using NLog;
using Omu.ValueInjecter;
using FluentValidation.Results;
using System.Net;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.Deluge
{
public class Deluge : TorrentClientBase<DelugeSettings>
{
private readonly IDelugeProxy _proxy;
public Deluge(IDelugeProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IParsingService parsingService,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, parsingService, remotePathMappingService, logger)
{
_proxy = proxy;
}
protected override String AddFromMagnetLink(RemoteEpisode remoteEpisode, String hash, String magnetLink)
{
var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings);
if (!Settings.TvCategory.IsNullOrWhiteSpace())
{
_proxy.SetLabel(actualHash, Settings.TvCategory, Settings);
}
_proxy.SetTorrentConfiguration(actualHash, "remove_at_ratio", false, Settings);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)DelugePriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)DelugePriority.First)
{
_proxy.MoveTorrentToTopInQueue(actualHash, Settings);
}
return actualHash.ToUpper();
}
protected override String AddFromTorrentFile(RemoteEpisode remoteEpisode, String hash, String filename, Byte[] fileContent)
{
var actualHash = _proxy.AddTorrentFromFile(filename, fileContent, Settings);
if (!Settings.TvCategory.IsNullOrWhiteSpace())
{
_proxy.SetLabel(actualHash, Settings.TvCategory, Settings);
}
_proxy.SetTorrentConfiguration(actualHash, "remove_at_ratio", false, Settings);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)DelugePriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)DelugePriority.First)
{
_proxy.MoveTorrentToTopInQueue(actualHash, Settings);
}
return actualHash.ToUpper();
}
public override IEnumerable<DownloadClientItem> GetItems()
{
IEnumerable<DelugeTorrent> torrents;
try
{
if (!Settings.TvCategory.IsNullOrWhiteSpace())
{
torrents = _proxy.GetTorrentsByLabel(Settings.TvCategory, Settings);
}
else
{
torrents = _proxy.GetTorrents(Settings);
}
}
catch (DownloadClientException ex)
{
_logger.ErrorException(ex.Message, ex);
return Enumerable.Empty<DownloadClientItem>();
}
var items = new List<DownloadClientItem>();
foreach (var torrent in torrents)
{
var item = new DownloadClientItem();
item.DownloadClientId = torrent.Hash.ToUpper();
item.Title = torrent.Name;
item.Category = Settings.TvCategory;
item.DownloadClient = Definition.Name;
item.DownloadTime = TimeSpan.FromSeconds(torrent.SecondsDownloading);
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath));
item.OutputPath = outputPath + torrent.Name;
item.RemainingSize = torrent.Size - torrent.BytesDownloaded;
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
item.TotalSize = torrent.Size;
if (torrent.State == DelugeTorrentStatus.Error)
{
item.Status = DownloadItemStatus.Failed;
}
else if (torrent.IsFinished && torrent.State != DelugeTorrentStatus.Checking)
{
item.Status = DownloadItemStatus.Completed;
}
else if (torrent.State == DelugeTorrentStatus.Queued)
{
item.Status = DownloadItemStatus.Queued;
}
else if (torrent.State == DelugeTorrentStatus.Paused)
{
item.Status = DownloadItemStatus.Paused;
}
else
{
item.Status = DownloadItemStatus.Downloading;
}
// Here we detect if Deluge is managing the torrent and whether the seed criteria has been met. This allows drone to delete the torrent as appropriate.
if (torrent.IsAutoManaged && torrent.StopAtRatio && torrent.Ratio >= torrent.StopRatio && torrent.State == DelugeTorrentStatus.Paused)
{
item.IsReadOnly = false;
}
else
{
item.IsReadOnly = true;
}
items.Add(item);
}
return items;
}
public override void RemoveItem(String hash)
{
_proxy.RemoveTorrent(hash.ToLower(), false, Settings);
}
public override String RetryDownload(String hash)
{
throw new NotSupportedException();
}
public override DownloadClientStatus GetStatus()
{
var config = _proxy.GetConfig(Settings);
var destDir = new OsPath(config.GetValueOrDefault("download_location") as string);
if (config.GetValueOrDefault("move_completed", false).ToString() == "True")
{
destDir = new OsPath(config.GetValueOrDefault("move_completed_path") as string);
}
var status = new DownloadClientStatus
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost"
};
if (!destDir.IsEmpty)
{
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) };
}
return status;
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
failures.AddIfNotNull(TestCategory());
failures.AddIfNotNull(TestGetTorrents());
}
private ValidationFailure TestConnection()
{
try
{
_proxy.GetVersion(Settings);
}
catch (DownloadClientAuthenticationException ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure("Password", "Authentication failed");
}
catch (WebException ex)
{
_logger.ErrorException(ex.Message, ex);
if (ex.Status == WebExceptionStatus.ConnectFailure)
{
return new NzbDroneValidationFailure("Host", "Unable to connect")
{
DetailedDescription = "Please verify the hostname and port."
};
}
else if (ex.Status == WebExceptionStatus.ConnectionClosed)
{
return new NzbDroneValidationFailure("UseSsl", "Verify SSL settings")
{
DetailedDescription = "Please verify your SSL configuration on both Deluge and NzbDrone."
};
}
else if (ex.Status == WebExceptionStatus.SecureChannelFailure)
{
return new NzbDroneValidationFailure("UseSsl", "Unable to connect through SSL")
{
DetailedDescription = "Drone is unable to connect to Deluge using SSL. This problem could be computer related. Please try to configure both drone and Deluge to not use SSL."
};
}
else
{
return new NzbDroneValidationFailure(String.Empty, "Unknown exception: " + ex.Message);
}
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure(String.Empty, "Unknown exception: " + ex.Message);
}
return null;
}
private ValidationFailure TestCategory()
{
if (Settings.TvCategory.IsNullOrWhiteSpace())
{
return null;
}
var enabledPlugins = _proxy.GetEnabledPlugins(Settings);
if (!enabledPlugins.Contains("Label"))
{
return new NzbDroneValidationFailure("TvCategory", "Label plugin not activated")
{
DetailedDescription = "You must have the Label plugin enabled in Deluge to use categories."
};
}
var labels = _proxy.GetAvailableLabels(Settings);
if (!labels.Contains(Settings.TvCategory))
{
_proxy.AddLabel(Settings.TvCategory, Settings);
labels = _proxy.GetAvailableLabels(Settings);
if (!labels.Contains(Settings.TvCategory))
{
return new NzbDroneValidationFailure("TvCategory", "Configuration of label failed")
{
DetailedDescription = "NzbDrone as unable to add the label to Deluge."
};
}
}
return null;
}
private ValidationFailure TestGetTorrents()
{
try
{
_proxy.GetTorrents(Settings);
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure(String.Empty, "Failed to get the list of torrents: " + ex.Message);
}
return null;
}
}
}

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Download.Clients.Deluge
{
public class DelugeError
{
public String Message { get; set; }
public Int32 Code { get; set; }
}
}

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Download.Clients.Deluge
{
public class DelugeException : DownloadClientException
{
public Int32 Code { get; set; }
public DelugeException(String message, Int32 code)
:base (message + " (code " + code + ")")
{
Code = code;
}
}
}

@ -0,0 +1,8 @@
namespace NzbDrone.Core.Download.Clients.Deluge
{
public enum DelugePriority
{
Last = 0,
First = 1
}
}

@ -0,0 +1,309 @@
using System;
using System.Net;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Rest;
using NLog;
using RestSharp;
using Newtonsoft.Json.Linq;
namespace NzbDrone.Core.Download.Clients.Deluge
{
public interface IDelugeProxy
{
String GetVersion(DelugeSettings settings);
Dictionary<String, Object> GetConfig(DelugeSettings settings);
DelugeTorrent[] GetTorrents(DelugeSettings settings);
DelugeTorrent[] GetTorrentsByLabel(String label, DelugeSettings settings);
String[] GetAvailablePlugins(DelugeSettings settings);
String[] GetEnabledPlugins(DelugeSettings settings);
String[] GetAvailableLabels(DelugeSettings settings);
void SetLabel(String hash, String label, DelugeSettings settings);
void SetTorrentConfiguration(String hash, String key, Object value, DelugeSettings settings);
void SetTorrentSeedingConfiguration(String hash, TorrentSeedConfiguration seedConfiguration, DelugeSettings settings);
void AddLabel(String label, DelugeSettings settings);
String AddTorrentFromMagnet(String magnetLink, DelugeSettings settings);
String AddTorrentFromFile(String filename, Byte[] fileContent, DelugeSettings settings);
Boolean RemoveTorrent(String hash, Boolean removeData, DelugeSettings settings);
void MoveTorrentToTopInQueue(String hash, DelugeSettings settings);
}
public class DelugeProxy : IDelugeProxy
{
private static readonly String[] requiredProperties = new String[] { "hash", "name", "state", "progress", "eta", "message", "is_finished", "save_path", "total_size", "total_done", "time_added", "active_time", "ratio", "is_auto_managed", "stop_at_ratio", "remove_at_ratio", "stop_ratio" };
private readonly Logger _logger;
private string _authPassword;
private CookieContainer _authCookieContainer;
private static Int32 _callId;
public DelugeProxy(Logger logger)
{
_logger = logger;
}
public String GetVersion(DelugeSettings settings)
{
var response = ProcessRequest<String>(settings, "daemon.info");
return response.Result;
}
public Dictionary<String, Object> GetConfig(DelugeSettings settings)
{
var response = ProcessRequest<Dictionary<String, Object>>(settings, "core.get_config");
return response.Result;
}
public DelugeTorrent[] GetTorrents(DelugeSettings settings)
{
var filter = new Dictionary<String, Object>();
// TODO: get_torrents_status returns the files as well, which starts to cause deluge timeouts when you get enough season packs.
//var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
var response = ProcessRequest<DelugeUpdateUIResult>(settings, "web.update_ui", requiredProperties, filter);
return response.Result.Torrents.Values.ToArray();
}
public DelugeTorrent[] GetTorrentsByLabel(String label, DelugeSettings settings)
{
var filter = new Dictionary<String, Object>();
filter.Add("label", label);
//var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
var response = ProcessRequest<DelugeUpdateUIResult>(settings, "web.update_ui", requiredProperties, filter);
return response.Result.Torrents.Values.ToArray();
}
public String AddTorrentFromMagnet(String magnetLink, DelugeSettings settings)
{
var response = ProcessRequest<String>(settings, "core.add_torrent_magnet", magnetLink, new JObject());
return response.Result;
}
public String AddTorrentFromFile(String filename, Byte[] fileContent, DelugeSettings settings)
{
var response = ProcessRequest<String>(settings, "core.add_torrent_file", filename, Convert.ToBase64String(fileContent), new JObject());
return response.Result;
}
public Boolean RemoveTorrent(String hashString, Boolean removeData, DelugeSettings settings)
{
var response = ProcessRequest<Boolean>(settings, "core.remove_torrent", hashString, removeData);
return response.Result;
}
public void MoveTorrentToTopInQueue(String hash, DelugeSettings settings)
{
ProcessRequest<Object>(settings, "core.queue_top", (Object)new String[] { hash });
}
public String[] GetAvailablePlugins(DelugeSettings settings)
{
var response = ProcessRequest<String[]>(settings, "core.get_available_plugins");
return response.Result;
}
public String[] GetEnabledPlugins(DelugeSettings settings)
{
var response = ProcessRequest<String[]>(settings, "core.get_enabled_plugins");
return response.Result;
}
public String[] GetAvailableLabels(DelugeSettings settings)
{
var response = ProcessRequest<String[]>(settings, "label.get_labels");
return response.Result;
}
public void SetTorrentConfiguration(String hash, String key, Object value, DelugeSettings settings)
{
var arguments = new Dictionary<String, Object>();
arguments.Add(key, value);
ProcessRequest<Object>(settings, "core.set_torrent_options", new String[] { hash }, arguments);
}
public void SetTorrentSeedingConfiguration(String hash, TorrentSeedConfiguration seedConfiguration, DelugeSettings settings)
{
if (seedConfiguration.Ratio != null)
{
var ratioArguments = new Dictionary<String, Object>();
ratioArguments.Add("stop_ratio", seedConfiguration.Ratio.Value);
ProcessRequest<Object>(settings, "core.set_torrent_options", new String[]{hash}, ratioArguments);
}
}
public void AddLabel(String label, DelugeSettings settings)
{
ProcessRequest<Object>(settings, "label.add", label);
}
public void SetLabel(String hash, String label, DelugeSettings settings)
{
ProcessRequest<Object>(settings, "label.set_torrent", hash, label);
}
protected DelugeResponse<TResult> ProcessRequest<TResult>(DelugeSettings settings, String action, params Object[] arguments)
{
var client = BuildClient(settings);
DelugeResponse<TResult> response;
try
{
response = ProcessRequest<TResult>(client, action, arguments);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.Timeout)
{
_logger.Debug("Deluge timeout during request, daemon connection may have been broken. Attempting to reconnect.");
response = new DelugeResponse<TResult>();
response.Error = new DelugeError();
response.Error.Code = 2;
}
else
{
throw;
}
}
if (response.Error != null)
{
if (response.Error.Code == 1 || response.Error.Code == 2)
{
AuthenticateClient(client);
response = ProcessRequest<TResult>(client, action, arguments);
if (response.Error == null)
{
return response;
}
throw new DownloadClientAuthenticationException(response.Error.Message);
}
throw new DelugeException(response.Error.Message, response.Error.Code);
}
return response;
}
private DelugeResponse<TResult> ProcessRequest<TResult>(IRestClient client, String action, Object[] arguments)
{
var request = new RestRequest(Method.POST);
request.Resource = "json";
request.RequestFormat = DataFormat.Json;
request.AddHeader("Accept-Encoding", "gzip,deflate");
var data = new Dictionary<String, Object>();
data.Add("id", GetCallId());
data.Add("method", action);
if (arguments != null)
{
data.Add("params", arguments);
}
request.AddBody(data);
_logger.Debug("Url: {0} Action: {1}", client.BuildUri(request), action);
var response = client.ExecuteAndValidate<DelugeResponse<TResult>>(request);
return response;
}
private IRestClient BuildClient(DelugeSettings settings)
{
var protocol = settings.UseSsl ? "https" : "http";
var url = String.Format(@"{0}://{1}:{2}",
protocol,
settings.Host,
settings.Port);
var restClient = RestClientFactory.BuildClient(url);
restClient.Timeout = 4000;
if (_authPassword != settings.Password || _authCookieContainer == null)
{
_authPassword = settings.Password;
AuthenticateClient(restClient);
}
else
{
restClient.CookieContainer = _authCookieContainer;
}
return restClient;
}
private void AuthenticateClient(IRestClient restClient)
{
restClient.CookieContainer = new CookieContainer();
var result = ProcessRequest<Boolean>(restClient, "auth.login", new Object[] { _authPassword });
if (!result.Result)
{
_logger.Debug("Deluge authentication failed.");
throw new DownloadClientAuthenticationException("Failed to authenticate with Deluge.");
}
else
{
_logger.Debug("Deluge authentication succeeded.");
_authCookieContainer = restClient.CookieContainer;
}
ConnectDaemon(restClient);
}
private void ConnectDaemon(IRestClient restClient)
{
var resultConnected = ProcessRequest<Boolean>(restClient, "web.connected", new Object[0]);
if (resultConnected.Result)
{
return;
}
var resultHosts = ProcessRequest<List<Object[]>>(restClient, "web.get_hosts", new Object[0]);
if (resultHosts.Result != null)
{
// The returned list contains the id, ip, port and status of each available connection. We want the 127.0.0.1
var connection = resultHosts.Result.Where(v => "127.0.0.1" == (v[1] as String)).FirstOrDefault();
if (connection != null)
{
ProcessRequest<Object>(restClient, "web.connect", new Object[] { connection[0] });
}
else
{
throw new DownloadClientException("Failed to connect to Deluge daemon.");
}
}
}
private Int32 GetCallId()
{
return System.Threading.Interlocked.Increment(ref _callId);
}
}
}

@ -0,0 +1,12 @@
using System;
using Newtonsoft.Json.Linq;
namespace NzbDrone.Core.Download.Clients.Deluge
{
public class DelugeResponse<TResult>
{
public Int32 Id { get; set; }
public TResult Result { get; set; }
public DelugeError Error { get; set; }
}
}

@ -0,0 +1,58 @@
using System;
using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Download.Clients.Deluge
{
public class DelugeSettingsValidator : AbstractValidator<DelugeSettings>
{
public DelugeSettingsValidator()
{
RuleFor(c => c.Host).NotEmpty();
RuleFor(c => c.Port).GreaterThan(0);
RuleFor(c => c.TvCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -");
}
}
public class DelugeSettings : IProviderConfig
{
private static readonly DelugeSettingsValidator validator = new DelugeSettingsValidator();
public DelugeSettings()
{
Host = "localhost";
Port = 8112;
Password = "deluge";
TvCategory = "tv-drone";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
public String Host { get; set; }
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public Int32 Port { get; set; }
[FieldDefinition(2, Label = "Password", Type = FieldType.Password)]
public String Password { get; set; }
[FieldDefinition(3, Label = "Category", Type = FieldType.Textbox)]
public String TvCategory { get; set; }
[FieldDefinition(4, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public Int32 RecentTvPriority { get; set; }
[FieldDefinition(5, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public Int32 OlderTvPriority { get; set; }
[FieldDefinition(6, Label = "Use SSL", Type = FieldType.Checkbox)]
public Boolean UseSsl { get; set; }
public ValidationResult Validate()
{
return validator.Validate(this);
}
}
}

@ -0,0 +1,59 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Download.Clients.Deluge
{
public class DelugeTorrent
{
public String Hash { get; set; }
public String Name { get; set; }
public String State { get; set; }
public Double Progress { get; set; }
public Double Eta { get; set; }
public String Message { get; set; }
[JsonProperty(PropertyName = "is_finished")]
public Boolean IsFinished { get; set; }
// Other paths: What is the difference between 'move_completed_path' and 'move_on_completed_path'?
/*
[JsonProperty(PropertyName = "move_completed_path")]
public String DownloadPathMoveCompleted { get; set; }
[JsonProperty(PropertyName = "move_on_completed_path")]
public String DownloadPathMoveOnCompleted { get; set; }
*/
[JsonProperty(PropertyName = "save_path")]
public String DownloadPath { get; set; }
[JsonProperty(PropertyName = "total_size")]
public Int64 Size { get; set; }
[JsonProperty(PropertyName = "total_done")]
public Int64 BytesDownloaded { get; set; }
[JsonProperty(PropertyName = "time_added")]
public Double DateAdded { get; set; }
[JsonProperty(PropertyName = "active_time")]
public Int32 SecondsDownloading { get; set; }
[JsonProperty(PropertyName = "ratio")]
public Double Ratio { get; set; }
[JsonProperty(PropertyName = "is_auto_managed")]
public Boolean IsAutoManaged { get; set; }
[JsonProperty(PropertyName = "stop_at_ratio")]
public Boolean StopAtRatio { get; set; }
[JsonProperty(PropertyName = "remove_at_ratio")]
public Boolean RemoveAtRatio { get; set; }
[JsonProperty(PropertyName = "stop_ratio")]
public Double StopRatio { get; set; }
}
}

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Download.Clients.Deluge
{
class DelugeTorrentStatus
{
public const String Paused = "Paused";
public const String Queued = "Queued";
public const String Downloading = "Downloading";
public const String Seeding = "Seeding";
public const String Checking = "Checking";
public const String Error = "Error";
}
}

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Download.Clients.Deluge
{
public class DelugeUpdateUIResult
{
public Dictionary<String, Object> Stats { get; set; }
public Boolean Connected { get; set; }
public Dictionary<String, DelugeTorrent> Torrents { get; set; }
}
}

@ -0,0 +1,31 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Download.Clients
{
public class DownloadClientAuthenticationException : DownloadClientException
{
public DownloadClientAuthenticationException(string message, params object[] args)
: base(message, args)
{
}
public DownloadClientAuthenticationException(string message)
: base(message)
{
}
public DownloadClientAuthenticationException(string message, Exception innerException, params object[] args)
: base(message, innerException, args)
{
}
public DownloadClientAuthenticationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

@ -131,7 +131,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
historyItem.DownloadClientId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString();
historyItem.Title = item.Name;
historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo);
historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, item.DestDir);
historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(item.DestDir));
historyItem.Category = item.Category;
historyItem.Message = String.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus);
historyItem.Status = DownloadItemStatus.Completed;
@ -215,7 +215,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
if (category != null)
{
status.OutputRootFolders = new List<String> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.DestDir) };
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(category.DestDir)) };
}
return status;
@ -321,10 +321,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
if (category != null)
{
var localPath = Settings.TvCategoryLocalPath;
var localPath = new OsPath(Settings.TvCategoryLocalPath);
Settings.TvCategoryLocalPath = null;
_remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, category.DestDir, localPath);
_remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, new OsPath(category.DestDir), localPath);
_logger.Info("Discovered Local Category Path for {0}, the setting was automatically moved to the Remote Path Mapping table.", Definition.Name);
}

@ -89,7 +89,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
TotalSize = _diskProvider.GetFileSize(file),
OutputPath = file
OutputPath = new OsPath(file)
};
if (_diskProvider.IsFileLocked(file))

@ -156,20 +156,20 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
historyItem.Status = DownloadItemStatus.Downloading;
}
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, sabHistoryItem.Storage);
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(sabHistoryItem.Storage));
if (!outputPath.IsNullOrWhiteSpace())
if (!outputPath.IsEmpty)
{
historyItem.OutputPath = outputPath;
var parent = outputPath.GetParentPath();
while (parent != null)
var parent = outputPath.Directory;
while (!parent.IsEmpty)
{
if (Path.GetFileName(parent) == sabHistoryItem.Title)
if (parent.FileName == sabHistoryItem.Title)
{
historyItem.OutputPath = parent;
}
parent = parent.GetParentPath();
parent = parent.Directory;
}
}
@ -259,52 +259,21 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
protected IEnumerable<SabnzbdCategory> GetCategories(SabnzbdConfig config)
{
var completeDir = config.Misc.complete_dir.TrimEnd('\\', '/');
var completeDir = new OsPath(config.Misc.complete_dir);
if (!completeDir.StartsWith("/") && !completeDir.StartsWith("\\") && !completeDir.Contains(':'))
if (!completeDir.IsRooted)
{
var queue = _proxy.GetQueue(0, 1, Settings);
var defaultRootFolder = new OsPath(queue.DefaultRootFolder);
if (queue.DefaultRootFolder.StartsWith("/"))
{
completeDir = queue.DefaultRootFolder + "/" + completeDir;
}
else
{
completeDir = queue.DefaultRootFolder + "\\" + completeDir;
}
completeDir = defaultRootFolder + completeDir;
}
foreach (var category in config.Categories)
{
var relativeDir = category.Dir.TrimEnd('*');
var relativeDir = new OsPath(category.Dir.TrimEnd('*'));
if (relativeDir.IsNullOrWhiteSpace())
{
category.FullPath = completeDir;
}
else if (completeDir.StartsWith("/"))
{ // Process remote Linux paths irrespective of our own OS.
if (relativeDir.StartsWith("/"))
{
category.FullPath = relativeDir;
}
else
{
category.FullPath = completeDir + "/" + relativeDir;
}
}
else
{ // Process remote Windows paths irrespective of our own OS.
if (relativeDir.StartsWith("\\") || relativeDir.Contains(':'))
{
category.FullPath = relativeDir;
}
else
{
category.FullPath = completeDir + "\\" + relativeDir;
}
}
category.FullPath = completeDir + relativeDir;
yield return category;
}
@ -329,7 +298,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
if (category != null)
{
status.OutputRootFolders = new List<String> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
status.OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
}
return status;
@ -454,7 +423,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
if (category != null)
{
var localPath = Settings.TvCategoryLocalPath;
var localPath = new OsPath(Settings.TvCategoryLocalPath);
Settings.TvCategoryLocalPath = null;
_remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, category.FullPath, localPath);

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
@ -30,6 +31,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public String Script { get; set; }
public String Dir { get; set; }
public String FullPath { get; set; }
public OsPath FullPath { get; set; }
}
}

@ -0,0 +1,153 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.MediaFiles;
using NLog;
using Omu.ValueInjecter;
using FluentValidation.Results;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.TorrentBlackhole
{
public class TorrentBlackhole : DownloadClientBase<TorrentBlackholeSettings>
{
private readonly IDiskScanService _diskScanService;
private readonly IHttpClient _httpClient;
public TorrentBlackhole(IDiskScanService diskScanService,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IParsingService parsingService,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(configService, diskProvider, remotePathMappingService, logger)
{
_diskScanService = diskScanService;
_httpClient = httpClient;
}
public override DownloadProtocol Protocol
{
get
{
return DownloadProtocol.Torrent;
}
}
public override string Download(RemoteEpisode remoteEpisode)
{
var url = remoteEpisode.Release.DownloadUrl;
var title = remoteEpisode.Release.Title;
title = FileNameBuilder.CleanFileName(title);
var filename = Path.Combine(Settings.TorrentFolder, String.Format("{0}.torrent", title));
_logger.Debug("Downloading torrent from: {0} to: {1}", url, filename);
_httpClient.DownloadFile(url, filename);
_logger.Debug("Torrent Download succeeded, saved to: {0}", filename);
return null;
}
public override IEnumerable<DownloadClientItem> GetItems()
{
foreach (var folder in _diskProvider.GetDirectories(Settings.WatchFolder))
{
var title = FileNameBuilder.CleanFileName(Path.GetFileName(folder));
var files = _diskProvider.GetFiles(folder, SearchOption.AllDirectories);
var historyItem = new DownloadClientItem
{
DownloadClient = Definition.Name,
DownloadClientId = Definition.Name + "_" + Path.GetFileName(folder) + "_" + _diskProvider.FolderGetCreationTime(folder).Ticks,
Category = "nzbdrone",
Title = title,
TotalSize = files.Select(_diskProvider.GetFileSize).Sum(),
OutputPath = new OsPath(folder)
};
if (files.Any(_diskProvider.IsFileLocked))
{
historyItem.Status = DownloadItemStatus.Downloading;
}
else
{
historyItem.Status = DownloadItemStatus.Completed;
historyItem.RemainingTime = TimeSpan.Zero;
}
yield return historyItem;
}
foreach (var videoFile in _diskScanService.GetVideoFiles(Settings.WatchFolder, false))
{
var title = FileNameBuilder.CleanFileName(Path.GetFileName(videoFile));
var historyItem = new DownloadClientItem
{
DownloadClient = Definition.Name,
DownloadClientId = Definition.Name + "_" + Path.GetFileName(videoFile) + "_" + _diskProvider.FileGetLastWrite(videoFile).Ticks,
Category = "nzbdrone",
Title = title,
TotalSize = _diskProvider.GetFileSize(videoFile),
OutputPath = new OsPath(videoFile)
};
if (_diskProvider.IsFileLocked(videoFile))
{
historyItem.Status = DownloadItemStatus.Downloading;
}
else
{
historyItem.Status = DownloadItemStatus.Completed;
historyItem.RemainingTime = TimeSpan.Zero;
}
yield return historyItem;
}
}
public override void RemoveItem(string id)
{
throw new NotSupportedException();
}
public override String RetryDownload(string id)
{
throw new NotSupportedException();
}
public override DownloadClientStatus GetStatus()
{
return new DownloadClientStatus
{
IsLocalhost = true,
OutputRootFolders = new List<OsPath> { new OsPath(Settings.WatchFolder) }
};
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestFolder(Settings.TorrentFolder, "TorrentFolder"));
failures.AddIfNotNull(TestFolder(Settings.WatchFolder, "WatchFolder"));
}
}
}

@ -0,0 +1,37 @@
using FluentValidation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentValidation.Results;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation.Paths;
namespace NzbDrone.Core.Download.Clients.TorrentBlackhole
{
public class TorrentBlackholeSettingsValidator : AbstractValidator<TorrentBlackholeSettings>
{
public TorrentBlackholeSettingsValidator()
{
//Todo: Validate that the path actually exists
RuleFor(c => c.TorrentFolder).IsValidPath();
}
}
public class TorrentBlackholeSettings : IProviderConfig
{
private static readonly TorrentBlackholeSettingsValidator validator = new TorrentBlackholeSettingsValidator();
[FieldDefinition(0, Label = "Torrent Folder", Type = FieldType.Path)]
public String TorrentFolder { get; set; }
[FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path)]
public String WatchFolder { get; set; }
public ValidationResult Validate()
{
return validator.Validate(this);
}
}
}

@ -0,0 +1,12 @@
using System;
namespace NzbDrone.Core.Download.Clients
{
public class TorrentSeedConfiguration
{
public static TorrentSeedConfiguration DefaultConfiguration = new TorrentSeedConfiguration();
public Double? Ratio { get; set; }
public TimeSpan? SeedTime { get; set; }
}
}

@ -0,0 +1,245 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Configuration;
using NLog;
using FluentValidation.Results;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Validation;
using System.Net;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.Transmission
{
public class Transmission : TorrentClientBase<TransmissionSettings>
{
private readonly ITransmissionProxy _proxy;
public Transmission(ITransmissionProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IParsingService parsingService,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, parsingService, remotePathMappingService, logger)
{
_proxy = proxy;
}
protected override String AddFromMagnetLink(RemoteEpisode remoteEpisode, String hash, String magnetLink)
{
_proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)TransmissionPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)TransmissionPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash, Settings);
}
return hash;
}
protected override String AddFromTorrentFile(RemoteEpisode remoteEpisode, String hash, String filename, Byte[] fileContent)
{
_proxy.AddTorrentFromData(fileContent, GetDownloadDirectory(), Settings);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)TransmissionPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)TransmissionPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash, Settings);
}
return hash;
}
private String GetDownloadDirectory()
{
if (Settings.TvCategory.IsNullOrWhiteSpace()) return null;
var config = _proxy.GetConfig(Settings);
var destDir = (String)config.GetValueOrDefault("download-dir");
return String.Format("{0}/.{1}", destDir, Settings.TvCategory);
}
public override IEnumerable<DownloadClientItem> GetItems()
{
List<TransmissionTorrent> torrents;
try
{
torrents = _proxy.GetTorrents(Settings);
}
catch (DownloadClientException ex)
{
_logger.ErrorException(ex.Message, ex);
return Enumerable.Empty<DownloadClientItem>();
}
var items = new List<DownloadClientItem>();
foreach (var torrent in torrents)
{
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadDir));
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
var directories = outputPath.FullPath.Split('\\', '/');
if (!directories.Contains(String.Format(".{0}", Settings.TvCategory))) continue;
}
var item = new DownloadClientItem();
item.DownloadClientId = torrent.HashString.ToUpper();
item.Category = Settings.TvCategory;
item.Title = torrent.Name;
item.DownloadClient = Definition.Name;
item.DownloadTime = TimeSpan.FromSeconds(torrent.SecondsDownloading);
item.Message = torrent.ErrorString;
item.OutputPath = outputPath + torrent.Name;
item.RemainingSize = torrent.LeftUntilDone;
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
item.TotalSize = torrent.TotalSize;
if (!torrent.ErrorString.IsNullOrWhiteSpace())
{
item.Status = DownloadItemStatus.Failed;
}
else if (torrent.Status == TransmissionTorrentStatus.Seeding || torrent.Status == TransmissionTorrentStatus.SeedingWait)
{
item.Status = DownloadItemStatus.Completed;
}
else if (torrent.IsFinished && torrent.Status != TransmissionTorrentStatus.Check && torrent.Status != TransmissionTorrentStatus.CheckWait)
{
item.Status = DownloadItemStatus.Completed;
}
else if (torrent.Status == TransmissionTorrentStatus.Queued)
{
item.Status = DownloadItemStatus.Queued;
}
else
{
item.Status = DownloadItemStatus.Downloading;
}
item.IsReadOnly = torrent.Status != TransmissionTorrentStatus.Stopped;
items.Add(item);
}
return items;
}
public override void RemoveItem(String hash)
{
_proxy.RemoveTorrent(hash.ToLower(), false, Settings);
}
public override String RetryDownload(String hash)
{
throw new NotSupportedException();
}
public override DownloadClientStatus GetStatus()
{
var config = _proxy.GetConfig(Settings);
var destDir = config.GetValueOrDefault("download-dir") as string;
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
destDir = String.Format("{0}/.{1}", destDir, Settings.TvCategory);
}
return new DownloadClientStatus
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(destDir)) }
};
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
failures.AddIfNotNull(TestGetTorrents());
}
private ValidationFailure TestConnection()
{
try
{
var versionString = _proxy.GetVersion(Settings);
_logger.Debug("Transmission version information: {0}", versionString);
var versionResult = Regex.Match(versionString, @"(?<!\(|(\d|\.)+)(\d|\.)+(?!\)|(\d|\.)+)").Value;
var version = Version.Parse(versionResult);
if (version < new Version(2, 40))
{
return new ValidationFailure(string.Empty, "Transmission version not supported, should be 2.40 or higher.");
}
}
catch (DownloadClientAuthenticationException ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure("Username", "Authentication failure")
{
DetailedDescription = "Please verify your username and password. Also verify if the host running NzbDrone isn't blocked from accessing Transmission by WhiteList limitations in the Transmission configuration."
};
}
catch (WebException ex)
{
_logger.ErrorException(ex.Message, ex);
if (ex.Status == WebExceptionStatus.ConnectFailure)
{
return new NzbDroneValidationFailure("Host", "Unable to connect")
{
DetailedDescription = "Please verify the hostname and port."
};
}
else
{
return new NzbDroneValidationFailure(String.Empty, "Unknown exception: " + ex.Message);
}
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure(String.Empty, "Unknown exception: " + ex.Message);
}
return null;
}
private ValidationFailure TestGetTorrents()
{
try
{
_proxy.GetTorrents(Settings);
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure(String.Empty, "Failed to get the list of torrents: " + ex.Message);
}
return null;
}
}
}

@ -0,0 +1,13 @@
using System;
namespace NzbDrone.Core.Download.Clients.Transmission
{
public class TransmissionException : DownloadClientException
{
public TransmissionException(String message)
: base(message)
{
}
}
}

@ -0,0 +1,8 @@
namespace NzbDrone.Core.Download.Clients.Transmission
{
public enum TransmissionPriority
{
Last = 0,
First = 1
}
}

@ -0,0 +1,284 @@
using System;
using System.Net;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Common;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Rest;
using NLog;
using RestSharp;
using Newtonsoft.Json.Linq;
namespace NzbDrone.Core.Download.Clients.Transmission
{
public interface ITransmissionProxy
{
List<TransmissionTorrent> GetTorrents(TransmissionSettings settings);
void AddTorrentFromUrl(String torrentUrl, String downloadDirectory, TransmissionSettings settings);
void AddTorrentFromData(Byte[] torrentData, String downloadDirectory, TransmissionSettings settings);
void SetTorrentSeedingConfiguration(String hash, TorrentSeedConfiguration seedConfiguration, TransmissionSettings settings);
Dictionary<String, Object> GetConfig(TransmissionSettings settings);
String GetVersion(TransmissionSettings settings);
void RemoveTorrent(String hash, Boolean removeData, TransmissionSettings settings);
void MoveTorrentToTopInQueue(String hashString, TransmissionSettings settings);
}
public class TransmissionProxy: ITransmissionProxy
{
private readonly Logger _logger;
private String _sessionId;
public TransmissionProxy(Logger logger)
{
_logger = logger;
}
public List<TransmissionTorrent> GetTorrents(TransmissionSettings settings)
{
var result = GetTorrentStatus(settings);
var torrents = ((JArray)result.Arguments["torrents"]).ToObject<List<TransmissionTorrent>>();
return torrents;
}
public void AddTorrentFromUrl(String torrentUrl, String downloadDirectory, TransmissionSettings settings)
{
var arguments = new Dictionary<String, Object>();
arguments.Add("filename", torrentUrl);
if (!downloadDirectory.IsNullOrWhiteSpace())
{
arguments.Add("download-dir", downloadDirectory);
}
ProcessRequest("torrent-add", arguments, settings);
}
public void AddTorrentFromData(Byte[] torrentData, String downloadDirectory, TransmissionSettings settings)
{
var arguments = new Dictionary<String, Object>();
arguments.Add("metainfo", Convert.ToBase64String(torrentData));
if (!downloadDirectory.IsNullOrWhiteSpace())
{
arguments.Add("download-dir", downloadDirectory);
}
ProcessRequest("torrent-add", arguments, settings);
}
public void SetTorrentSeedingConfiguration(String hash, TorrentSeedConfiguration seedConfiguration, TransmissionSettings settings)
{
var arguments = new Dictionary<String, Object>();
arguments.Add("ids", new String[] { hash });
if (seedConfiguration.Ratio != null)
{
arguments.Add("seedRatioLimit", seedConfiguration.Ratio.Value);
arguments.Add("seedRatioMode", 1);
}
if (seedConfiguration.SeedTime != null)
{
arguments.Add("seedIdleLimit", Convert.ToInt32(seedConfiguration.SeedTime.Value.TotalMinutes));
arguments.Add("seedIdleMode", 1);
}
ProcessRequest("torrent-set", arguments, settings);
}
public String GetVersion(TransmissionSettings settings)
{
// Gets the transmission version.
var config = GetConfig(settings);
var version = config["version"];
return version.ToString();
}
public Dictionary<String, Object> GetConfig(TransmissionSettings settings)
{
// Gets the transmission version.
var result = GetSessionVariables(settings);
return result.Arguments;
}
public void RemoveTorrent(String hashString, Boolean removeData, TransmissionSettings settings)
{
var arguments = new Dictionary<String, Object>();
arguments.Add("ids", new String[] { hashString });
arguments.Add("delete-local-data", removeData);
ProcessRequest("torrent-remove", arguments, settings);
}
public void MoveTorrentToTopInQueue(String hashString, TransmissionSettings settings)
{
var arguments = new Dictionary<String, Object>();
arguments.Add("ids", new String[] { hashString });
ProcessRequest("queue-move-top", arguments, settings);
}
private TransmissionResponse GetSessionVariables(TransmissionSettings settings)
{
// Retrieve transmission information such as the default download directory, bandwith throttling and seed ratio.
return ProcessRequest("session-get", null, settings);
}
private TransmissionResponse GetSessionStatistics(TransmissionSettings settings)
{
return ProcessRequest("session-stats", null, settings);
}
private TransmissionResponse GetTorrentStatus(TransmissionSettings settings)
{
return GetTorrentStatus(null, settings);
}
private TransmissionResponse GetTorrentStatus(IEnumerable<String> hashStrings, TransmissionSettings settings)
{
var fields = new String[]{
"id",
"hashString", // Unique torrent ID. Use this instead of the client id?
"name",
"downloadDir",
"status",
"totalSize",
"leftUntilDone",
"isFinished",
"eta",
"errorString"
};
var arguments = new Dictionary<String, Object>();
arguments.Add("fields", fields);
if (hashStrings != null)
{
arguments.Add("ids", hashStrings);
}
var result = ProcessRequest("torrent-get", arguments, settings);
return result;
}
protected String GetSessionId(IRestClient client, TransmissionSettings settings)
{
var request = new RestRequest();
request.RequestFormat = DataFormat.Json;
_logger.Debug("Url: {0} GetSessionId", client.BuildUri(request));
var restResponse = client.Execute(request);
if (restResponse.StatusCode == HttpStatusCode.MovedPermanently)
{
var uri = new Uri(restResponse.ResponseUri, (String)restResponse.GetHeaderValue("Location"));
throw new DownloadClientException("Remote site redirected to " + uri);
}
// We expect the StatusCode = Conflict, coz that will provide us with a new session id.
if (restResponse.StatusCode == HttpStatusCode.Conflict)
{
var sessionId = restResponse.Headers.SingleOrDefault(o => o.Name == "X-Transmission-Session-Id");
if (sessionId == null)
{
throw new DownloadClientException("Remote host did not return a Session Id.");
}
return (String)sessionId.Value;
}
else if (restResponse.StatusCode == HttpStatusCode.Unauthorized)
{
throw new DownloadClientAuthenticationException("User authentication failed.");
}
restResponse.ValidateResponse(client);
throw new DownloadClientException("Remote host did not return a Session Id.");
}
public TransmissionResponse ProcessRequest(String action, Object arguments, TransmissionSettings settings)
{
var client = BuildClient(settings);
if (String.IsNullOrWhiteSpace(_sessionId))
{
_sessionId = GetSessionId(client, settings);
}
var request = new RestRequest(Method.POST);
request.RequestFormat = DataFormat.Json;
request.AddHeader("X-Transmission-Session-Id", _sessionId);
var data = new Dictionary<String, Object>();
data.Add("method", action);
if (arguments != null)
{
data.Add("arguments", arguments);
}
request.AddBody(data);
_logger.Debug("Url: {0} Action: {1}", client.BuildUri(request), action);
var restResponse = client.Execute(request);
if (restResponse.StatusCode == HttpStatusCode.Conflict)
{
_sessionId = GetSessionId(client, settings);
request.Parameters.First(o => o.Name == "X-Transmission-Session-Id").Value = _sessionId;
restResponse = client.Execute(request);
}
else if (restResponse.StatusCode == HttpStatusCode.Unauthorized)
{
throw new DownloadClientAuthenticationException("User authentication failed.");
}
var transmissionResponse = restResponse.Read<TransmissionResponse>(client);
if (transmissionResponse == null)
{
throw new TransmissionException("Unexpected response");
}
else if (transmissionResponse.Result != "success")
{
throw new TransmissionException(transmissionResponse.Result);
}
return transmissionResponse;
}
private IRestClient BuildClient(TransmissionSettings settings)
{
var protocol = settings.UseSsl ? "https" : "http";
String url;
if (!settings.UrlBase.IsNullOrWhiteSpace())
{
url = String.Format(@"{0}://{1}:{2}/{3}/transmission/rpc", protocol, settings.Host, settings.Port, settings.UrlBase.Trim('/'));
}
else
{
url = String.Format(@"{0}://{1}:{2}/transmission/rpc", protocol, settings.Host, settings.Port);
}
var restClient = RestClientFactory.BuildClient(url);
restClient.FollowRedirects = false;
if (!settings.Username.IsNullOrWhiteSpace())
{
restClient.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password);
}
return restClient;
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save