mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-05 02:22:31 +01:00
New: Implemented Torrent Download Clients: uTorrent, Transmission and Deluge. And several public and private Torrent Indexers.
This commit is contained in:
parent
ffa814f387
commit
67cd5703a1
321
src/MonoTorrent/BEncoding/BEncodedDictionary.cs
Normal file
321
src/MonoTorrent/BEncoding/BEncodedDictionary.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
219
src/MonoTorrent/BEncoding/BEncodedList.cs
Normal file
219
src/MonoTorrent/BEncoding/BEncodedList.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
209
src/MonoTorrent/BEncoding/BEncodedNumber.cs
Normal file
209
src/MonoTorrent/BEncoding/BEncodedNumber.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
220
src/MonoTorrent/BEncoding/BEncodedString.cs
Normal file
220
src/MonoTorrent/BEncoding/BEncodedString.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
30
src/MonoTorrent/BEncoding/BEncodingException.cs
Normal file
30
src/MonoTorrent/BEncoding/BEncodingException.cs
Normal file
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
203
src/MonoTorrent/BEncoding/IBEncodedValue.cs
Normal file
203
src/MonoTorrent/BEncoding/IBEncodedValue.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
129
src/MonoTorrent/BEncoding/RawReader.cs
Normal file
129
src/MonoTorrent/BEncoding/RawReader.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
420
src/MonoTorrent/BitField.cs
Normal file
420
src/MonoTorrent/BitField.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
235
src/MonoTorrent/Check.cs
Normal file
235
src/MonoTorrent/Check.cs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
13
src/MonoTorrent/Enums.cs
Normal file
13
src/MonoTorrent/Enums.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace MonoTorrent
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
DoNotDownload = 0,
|
||||
Lowest = 1,
|
||||
Low = 2,
|
||||
Normal = 4,
|
||||
High = 8,
|
||||
Highest = 16,
|
||||
Immediate = 32
|
||||
}
|
||||
}
|
30
src/MonoTorrent/Exceptions/MessageException.cs
Normal file
30
src/MonoTorrent/Exceptions/MessageException.cs
Normal file
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
41
src/MonoTorrent/HashAlgoFactory.cs
Normal file
41
src/MonoTorrent/HashAlgoFactory.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
93
src/MonoTorrent/Hashes.cs
Normal file
93
src/MonoTorrent/Hashes.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
171
src/MonoTorrent/InfoHash.cs
Normal file
171
src/MonoTorrent/InfoHash.cs
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
95
src/MonoTorrent/MagnetLink.cs
Normal file
95
src/MonoTorrent/MagnetLink.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
src/MonoTorrent/Messages/IMessage.cs
Normal file
12
src/MonoTorrent/Messages/IMessage.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
164
src/MonoTorrent/Messages/Message.cs
Normal file
164
src/MonoTorrent/Messages/Message.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
117
src/MonoTorrent/MonoTorrent.csproj
Normal file
117
src/MonoTorrent/MonoTorrent.csproj
Normal file
@ -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>
|
97
src/MonoTorrent/RawTrackerTier.cs
Normal file
97
src/MonoTorrent/RawTrackerTier.cs
Normal file
@ -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 ();
|
||||
}
|
||||
}
|
||||
}
|
105
src/MonoTorrent/RawTrackerTiers.cs
Normal file
105
src/MonoTorrent/RawTrackerTiers.cs
Normal file
@ -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 ();
|
||||
}
|
||||
}
|
||||
}
|
125
src/MonoTorrent/ToolBox.cs
Normal file
125
src/MonoTorrent/ToolBox.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
885
src/MonoTorrent/Torrent.cs
Normal file
885
src/MonoTorrent/Torrent.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
28
src/MonoTorrent/TorrentException.cs
Normal file
28
src/MonoTorrent/TorrentException.cs
Normal file
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
205
src/MonoTorrent/TorrentFile.cs
Normal file
205
src/MonoTorrent/TorrentFile.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
153
src/MonoTorrent/UriHelper.cs
Normal file
153
src/MonoTorrent/UriHelper.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 @@ public class CleanseLogMessageFixture
|
||||
[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);
|
||||
|
@ -23,5 +23,10 @@ public static Dictionary<T1, T2> Merge<T1, T2>(this Dictionary<T1, T2> first, Di
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
public static void Add<TKey, TValue>(this ICollection<KeyValuePair<TKey, TValue>> collection, TKey key, TValue value)
|
||||
{
|
||||
collection.Add(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
44
src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs
Normal file
44
src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs
Normal file
@ -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, 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);
|
||||
}
|
||||
}
|
||||
}
|
16
src/NzbDrone.Common/Http/JsonRpcResponse.cs
Normal file
16
src/NzbDrone.Common/Http/JsonRpcResponse.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
@ -7,15 +7,29 @@ public class CleanseLogMessage
|
||||
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.*?)(?<=\?|&|;)(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),
|
||||
|
||||
// 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)
|
||||
};
|
||||
|
||||
//private static readonly Regex CleansingRegex = new Regex(@"(?<=apikey=)(\w+?)(?=\W|$|_)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
@ -140,6 +140,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 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 @@ private void WithRetention(int days)
|
||||
|
||||
private void WithAge(int days)
|
||||
{
|
||||
parseResult.Release.PublishDate = DateTime.Now.AddDays(-days);
|
||||
_remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-days);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -40,7 +41,7 @@ public void should_return_true_when_retention_is_set_to_zero()
|
||||
WithRetention(0);
|
||||
WithAge(100);
|
||||
|
||||
Subject.IsSatisfiedBy(parseResult, null).Accepted.Should().BeTrue();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -49,7 +50,7 @@ public void should_return_true_when_release_if_younger_than_retention()
|
||||
WithRetention(1000);
|
||||
WithAge(100);
|
||||
|
||||
Subject.IsSatisfiedBy(parseResult, null).Accepted.Should().BeTrue();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -58,7 +59,7 @@ public void should_return_true_when_release_and_retention_are_the_same()
|
||||
WithRetention(100);
|
||||
WithAge(100);
|
||||
|
||||
Subject.IsSatisfiedBy(parseResult, null).Accepted.Should().BeTrue();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -67,7 +68,7 @@ public void should_return_false_when_old_than_retention()
|
||||
WithRetention(10);
|
||||
WithAge(100);
|
||||
|
||||
Subject.IsSatisfiedBy(parseResult, null).Accepted.Should().BeFalse();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -76,7 +77,18 @@ public void should_return_true_if_release_came_out_today_and_retention_is_zero()
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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 @@ protected void GivenFailedDownload()
|
||||
protected void GivenCompletedItem()
|
||||
{
|
||||
var targetDir = Path.Combine(_completedDownloadFolder, _title);
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(c => c.GetDirectories(_completedDownloadFolder))
|
||||
.Returns(new[] { targetDir });
|
||||
@ -69,6 +70,17 @@ public void completed_download_should_have_required_properties()
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,350 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
65
src/NzbDrone.Core.Test/Files/RSS/BitMeTv.xml
Normal file
65
src/NzbDrone.Core.Test/Files/RSS/BitMeTv.xml
Normal file
@ -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>
|
63
src/NzbDrone.Core.Test/Files/RSS/Eztv.xml
Normal file
63
src/NzbDrone.Core.Test/Files/RSS/Eztv.xml
Normal file
@ -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>
|
35
src/NzbDrone.Core.Test/Files/RSS/IPTorrents.xml
Normal file
35
src/NzbDrone.Core.Test/Files/RSS/IPTorrents.xml
Normal file
@ -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's Baby S01E01 Part 1 1080p WEB-DL DD5 1 H 264-BS</title>
|
||||
<link>http://iptorrents.com/download.php/1234/Rosemary'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's Baby S01E01 Part 1 720p WEB-DL DD5 1 H 264-BS</title>
|
||||
<link>http://iptorrents.com/download.php/1234/Rosemary'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>
|
93
src/NzbDrone.Core.Test/Files/RSS/KickassTorrents.xml
Normal file
93
src/NzbDrone.Core.Test/Files/RSS/KickassTorrents.xml
Normal file
@ -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>
|
40
src/NzbDrone.Core.Test/Files/RSS/Nyaa.xml
Normal file
40
src/NzbDrone.Core.Test/Files/RSS/Nyaa.xml
Normal file
@ -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 (TBS).ts</title>
|
||||
<category>Raw Anime</category>
|
||||
<link>http://www.nyaa.se/?page=download&tid=587750</link>
|
||||
<guid>http://www.nyaa.se/?page=view&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&tid=587749</link>
|
||||
<guid>http://www.nyaa.se/?page=view&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 (TBS 1280x720 x264 AAC).mp4</title>
|
||||
<category>Raw Anime</category>
|
||||
<link>http://www.nyaa.se/?page=download&tid=587748</link>
|
||||
<guid>http://www.nyaa.se/?page=view&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&tid=587747</link>
|
||||
<guid>http://www.nyaa.se/?page=view&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>
|
56
src/NzbDrone.Core.Test/Files/RSS/Torrentleech.xml
Normal file
56
src/NzbDrone.Core.Test/Files/RSS/Torrentleech.xml
Normal file
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
63
src/NzbDrone.Core.Test/IndexerTests/EztvTests/EztvFixture.cs
Normal file
63
src/NzbDrone.Core.Test/IndexerTests/EztvTests/EztvFixture.cs
Normal file
@ -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,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Eztv;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
using NzbDrone.Core.Indexers.Wombles;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@ -34,7 +35,7 @@ public void wombles_rss()
|
||||
|
||||
ValidateResult(result, skipSize: true, skipInfo: true);
|
||||
}
|
||||
|
||||
|
||||
private void ValidateResult(IList<ReleaseInfo> reports, bool skipSize = false, bool skipInfo = false)
|
||||
{
|
||||
reports.Should().NotBeEmpty();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
57
src/NzbDrone.Core.Test/IndexerTests/NyaaTests/NyaaFixture.cs
Normal file
57
src/NzbDrone.Core.Test/IndexerTests/NyaaTests/NyaaFixture.cs
Normal file
@ -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 @@ public class ParserFixture : CoreTest
|
||||
[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);
|
||||
|
@ -23,6 +23,8 @@ public class ReleaseGroupParserFixture : CoreTest
|
||||
[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 @@ public class SingleEpisodeParserFixture : CoreTest
|
||||
[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 @@ public class SingleEpisodeParserFixture : CoreTest
|
||||
[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 @@ public void Setup()
|
||||
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()
|
||||
@ -112,5 +116,24 @@ public void should_remap_local_to_remote(String host, String expectedRemotePath,
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 @@ public List<DownloadDecision> PrioritizeDecisions(List<DownloadDecision> decisio
|
||||
.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 @@ public RetentionSpecification(IConfigService configService, Logger logger)
|
||||
|
||||
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,51 @@
|
||||
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 string RejectionReason
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Not enough Torrent seeders";
|
||||
}
|
||||
}
|
||||
|
||||
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("Only {0} seeders, skipping.", torrentInfo.Seeds);
|
||||
return Decision.Reject("No seeders");
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
304
src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs
Normal file
304
src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs
Normal file
@ -0,0 +1,304 @@
|
||||
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, torrent.DownloadPath);
|
||||
item.OutputPath = Path.Combine(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 = config.GetValueOrDefault("download_location") as string;
|
||||
|
||||
if (config.GetValueOrDefault("move_completed", false).ToString() == "True")
|
||||
{
|
||||
destDir = config.GetValueOrDefault("move_completed_path") as string;
|
||||
}
|
||||
|
||||
var status = new DownloadClientStatus
|
||||
{
|
||||
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost"
|
||||
};
|
||||
|
||||
if (destDir != null)
|
||||
{
|
||||
status.OutputRootFolders = new List<string> { _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;
|
||||
}
|
||||
}
|
||||
}
|
13
src/NzbDrone.Core/Download/Clients/Deluge/DelugeError.cs
Normal file
13
src/NzbDrone.Core/Download/Clients/Deluge/DelugeError.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
18
src/NzbDrone.Core/Download/Clients/Deluge/DelugeException.cs
Normal file
18
src/NzbDrone.Core/Download/Clients/Deluge/DelugeException.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
{
|
||||
public enum DelugePriority
|
||||
{
|
||||
Last = 0,
|
||||
First = 1
|
||||
}
|
||||
}
|
303
src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs
Normal file
303
src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs
Normal file
@ -0,0 +1,303 @@
|
||||
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 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>();
|
||||
|
||||
var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
|
||||
|
||||
return response.Result.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]);
|
||||
|
||||
return response.Result.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);
|
||||
}
|
||||
}
|
||||
}
|
12
src/NzbDrone.Core/Download/Clients/Deluge/DelugeResponse.cs
Normal file
12
src/NzbDrone.Core/Download/Clients/Deluge/DelugeResponse.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
58
src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs
Normal file
58
src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
59
src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrent.cs
Normal file
59
src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrent.cs
Normal file
@ -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,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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = 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 = 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<string> { 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; }
|
||||
}
|
||||
}
|
258
src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs
Normal file
258
src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs
Normal file
@ -0,0 +1,258 @@
|
||||
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 = torrent.DownloadDir;
|
||||
|
||||
// Transmission always returns path with forward slashes, even on windows.
|
||||
if (outputPath.IsNotNullOrWhiteSpace() && (outputPath.StartsWith(@"\\") || outputPath.Contains(':')))
|
||||
{
|
||||
outputPath = outputPath.Replace('/', '\\');
|
||||
}
|
||||
|
||||
outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath);
|
||||
|
||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var directories = outputPath.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 = Path.Combine(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);
|
||||
}
|
||||
|
||||
// Transmission always returns path with forward slashes, even on windows.
|
||||
if (destDir.StartsWith(@"\\") || destDir.Contains(':'))
|
||||
{
|
||||
destDir = destDir.Replace('/', '\\');
|
||||
}
|
||||
|
||||
return new DownloadClientStatus
|
||||
{
|
||||
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
|
||||
OutputRootFolders = new List<string> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestConnection());
|
||||
if (failures.Any())
|
||||
return;
|
||||
failures.AddIfNotNull(TestGetTorrents());
|
||||
}
|
||||
|
||||
private ValidationFailure TestConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
var versionResult =
|
||||
Regex.Replace(_proxy.GetVersion(Settings), @"\([^)]*\)", "",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Multiline).Trim();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
{
|
||||
public class TransmissionResponse
|
||||
{
|
||||
public String Result { get; set; }
|
||||
public Dictionary<String, Object> Arguments { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
{
|
||||
public class TransmissionSettingsValidator : AbstractValidator<TransmissionSettings>
|
||||
{
|
||||
public TransmissionSettingsValidator()
|
||||
{
|
||||
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 TransmissionSettings : IProviderConfig
|
||||
{
|
||||
private static readonly TransmissionSettingsValidator validator = new TransmissionSettingsValidator();
|
||||
|
||||
public TransmissionSettings()
|
||||
{
|
||||
Host = "localhost";
|
||||
Port = 9091;
|
||||
}
|
||||
|
||||
[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 = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the transmission rpc url, see http://[host]:[port]/[urlBase]/transmission/rpc")]
|
||||
public String UrlBase { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)]
|
||||
public String Username { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)]
|
||||
public String Password { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)]
|
||||
public String TvCategory { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
|
||||
public Int32 RecentTvPriority { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
|
||||
public Int32 OlderTvPriority { get; set; }
|
||||
|
||||
[FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)]
|
||||
public Boolean UseSsl { get; set; }
|
||||
|
||||
public ValidationResult Validate()
|
||||
{
|
||||
return validator.Validate(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
{
|
||||
public class TransmissionTorrent
|
||||
{
|
||||
public Int32 Id { get; set; }
|
||||
|
||||
public String HashString { get; set; }
|
||||
|
||||
public String Name { get; set; }
|
||||
|
||||
public String DownloadDir { get; set; }
|
||||
|
||||
public Int64 TotalSize { get; set; }
|
||||
|
||||
public Int64 LeftUntilDone { get; set; }
|
||||
|
||||
public Boolean IsFinished { get; set; }
|
||||
|
||||
public Int32 Eta { get; set; }
|
||||
|
||||
public TransmissionTorrentStatus Status { get; set; }
|
||||
|
||||
public Int32 SecondsDownloading { get; set; }
|
||||
|
||||
public String ErrorString { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
{
|
||||
public enum TransmissionTorrentStatus
|
||||
{
|
||||
Stopped = 0,
|
||||
CheckWait = 1,
|
||||
Check = 2,
|
||||
Queued = 3,
|
||||
Downloading = 4,
|
||||
SeedingWait = 5,
|
||||
Seeding = 6
|
||||
}
|
||||
}
|
@ -70,6 +70,7 @@ public override IEnumerable<DownloadClientItem> GetItems()
|
||||
{
|
||||
DownloadClient = Definition.Name,
|
||||
DownloadClientId = Definition.Name + "_" + Path.GetFileName(folder) + "_" + _diskProvider.FolderGetCreationTime(folder).Ticks,
|
||||
Category = "nzbdrone",
|
||||
Title = title,
|
||||
|
||||
TotalSize = files.Select(_diskProvider.GetFileSize).Sum(),
|
||||
@ -99,6 +100,7 @@ public override IEnumerable<DownloadClientItem> GetItems()
|
||||
{
|
||||
DownloadClient = Definition.Name,
|
||||
DownloadClientId = Definition.Name + "_" + Path.GetFileName(videoFile) + "_" + _diskProvider.FileGetLastWrite(videoFile).Ticks,
|
||||
Category = "nzbdrone",
|
||||
Title = title,
|
||||
|
||||
TotalSize = _diskProvider.GetFileSize(videoFile),
|
||||
|
261
src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs
Normal file
261
src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs
Normal file
@ -0,0 +1,261 @@
|
||||
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.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Validation;
|
||||
using FluentValidation.Results;
|
||||
using System.Net;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.RemotePathMappings;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
{
|
||||
public class UTorrent : TorrentClientBase<UTorrentSettings>
|
||||
{
|
||||
private readonly IUTorrentProxy _proxy;
|
||||
|
||||
public UTorrent(IUTorrentProxy 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, Settings);
|
||||
_proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings);
|
||||
|
||||
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
|
||||
|
||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)UTorrentPriority.First ||
|
||||
!isRecentEpisode && Settings.OlderTvPriority == (int)UTorrentPriority.First)
|
||||
{
|
||||
_proxy.MoveTorrentToTopInQueue(hash, Settings);
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
protected override String AddFromTorrentFile(RemoteEpisode remoteEpisode, String hash, String filename, Byte[] fileContent)
|
||||
{
|
||||
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
|
||||
_proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings);
|
||||
|
||||
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
|
||||
|
||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)UTorrentPriority.First ||
|
||||
!isRecentEpisode && Settings.OlderTvPriority == (int)UTorrentPriority.First)
|
||||
{
|
||||
_proxy.MoveTorrentToTopInQueue(hash, Settings);
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public override IEnumerable<DownloadClientItem> GetItems()
|
||||
{
|
||||
List<UTorrentTorrent> torrents;
|
||||
|
||||
try
|
||||
{
|
||||
torrents = _proxy.GetTorrents(Settings);
|
||||
}
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
_logger.ErrorException(ex.Message, ex);
|
||||
return Enumerable.Empty<DownloadClientItem>();
|
||||
}
|
||||
|
||||
var queueItems = new List<DownloadClientItem>();
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
if (torrent.Label != Settings.TvCategory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = new DownloadClientItem();
|
||||
item.DownloadClientId = torrent.Hash;
|
||||
item.Title = torrent.Name;
|
||||
item.TotalSize = torrent.Size;
|
||||
item.Category = torrent.Label;
|
||||
item.DownloadClient = Definition.Name;
|
||||
item.RemainingSize = torrent.Remaining;
|
||||
if (torrent.Eta != -1)
|
||||
{
|
||||
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
|
||||
}
|
||||
|
||||
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, torrent.RootDownloadPath);
|
||||
|
||||
if (outputPath == null || Path.GetFileName(outputPath) == torrent.Name)
|
||||
{
|
||||
item.OutputPath = outputPath;
|
||||
}
|
||||
else if (OsInfo.IsWindows && outputPath.EndsWith(":"))
|
||||
{
|
||||
item.OutputPath = Path.Combine(outputPath + Path.DirectorySeparatorChar, torrent.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.OutputPath = Path.Combine(outputPath, torrent.Name);
|
||||
}
|
||||
|
||||
if (torrent.Status.HasFlag(UTorrentTorrentStatus.Error))
|
||||
{
|
||||
item.Status = DownloadItemStatus.Failed;
|
||||
}
|
||||
else if (torrent.Status.HasFlag(UTorrentTorrentStatus.Loaded) &&
|
||||
torrent.Status.HasFlag(UTorrentTorrentStatus.Checked) && torrent.Remaining == 0 && torrent.Progress == 1.0)
|
||||
{
|
||||
item.Status = DownloadItemStatus.Completed;
|
||||
}
|
||||
else if (torrent.Status.HasFlag(UTorrentTorrentStatus.Paused))
|
||||
{
|
||||
item.Status = DownloadItemStatus.Paused;
|
||||
}
|
||||
else if (torrent.Status.HasFlag(UTorrentTorrentStatus.Started))
|
||||
{
|
||||
item.Status = DownloadItemStatus.Downloading;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Status = DownloadItemStatus.Queued;
|
||||
}
|
||||
|
||||
// 'Started' without 'Queued' is when the torrent is 'forced seeding'
|
||||
item.IsReadOnly = torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) || torrent.Status.HasFlag(UTorrentTorrentStatus.Started);
|
||||
|
||||
queueItems.Add(item);
|
||||
}
|
||||
|
||||
return queueItems;
|
||||
}
|
||||
|
||||
public override void RemoveItem(String id)
|
||||
{
|
||||
_proxy.RemoveTorrent(id, false, Settings);
|
||||
}
|
||||
|
||||
public override String RetryDownload(String id)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override DownloadClientStatus GetStatus()
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
|
||||
String destDir = null;
|
||||
|
||||
if (config.GetValueOrDefault("dir_active_download_flag") == "true")
|
||||
{
|
||||
destDir = config.GetValueOrDefault("dir_active_download");
|
||||
}
|
||||
|
||||
if (config.GetValueOrDefault("dir_completed_download_flag") == "true")
|
||||
{
|
||||
destDir = config.GetValueOrDefault("dir_completed_download");
|
||||
|
||||
if (config.GetValueOrDefault("dir_add_label") == "true")
|
||||
{
|
||||
destDir = Path.Combine(destDir, Settings.TvCategory);
|
||||
}
|
||||
}
|
||||
|
||||
var status = new DownloadClientStatus
|
||||
{
|
||||
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost"
|
||||
};
|
||||
|
||||
if (destDir != null)
|
||||
{
|
||||
status.OutputRootFolders = new List<String> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) };
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestConnection());
|
||||
if (failures.Any())
|
||||
return;
|
||||
failures.AddIfNotNull(TestGetTorrents());
|
||||
}
|
||||
|
||||
private ValidationFailure TestConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = _proxy.GetVersion(Settings);
|
||||
|
||||
if (version < 25406)
|
||||
{
|
||||
return new ValidationFailure(string.Empty, "Old uTorrent client with unsupported API, need 3.0 or higher");
|
||||
}
|
||||
}
|
||||
catch (DownloadClientAuthenticationException ex)
|
||||
{
|
||||
_logger.ErrorException(ex.Message, ex);
|
||||
return new NzbDroneValidationFailure("Username", "Authentication failure")
|
||||
{
|
||||
DetailedDescription = "Please verify your username and password."
|
||||
};
|
||||
}
|
||||
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,8 @@
|
||||
namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
{
|
||||
public enum UTorrentPriority
|
||||
{
|
||||
Last = 0,
|
||||
First = 1
|
||||
}
|
||||
}
|
261
src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs
Normal file
261
src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs
Normal file
@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Rest;
|
||||
using RestSharp;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
{
|
||||
public interface IUTorrentProxy
|
||||
{
|
||||
Int32 GetVersion(UTorrentSettings settings);
|
||||
Dictionary<String, String> GetConfig(UTorrentSettings settings);
|
||||
List<UTorrentTorrent> GetTorrents(UTorrentSettings settings);
|
||||
|
||||
void AddTorrentFromUrl(String torrentUrl, UTorrentSettings settings);
|
||||
void AddTorrentFromFile(String fileName, Byte[] fileContent, UTorrentSettings settings);
|
||||
void SetTorrentSeedingConfiguration(String hash, TorrentSeedConfiguration seedConfiguration, UTorrentSettings settings);
|
||||
|
||||
void RemoveTorrent(String hash, Boolean removeData, UTorrentSettings settings);
|
||||
void SetTorrentLabel(String hash, String label, UTorrentSettings settings);
|
||||
void MoveTorrentToTopInQueue(String hash, UTorrentSettings settings);
|
||||
}
|
||||
|
||||
public class UTorrentProxy : IUTorrentProxy
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly CookieContainer _cookieContainer;
|
||||
private String _authToken;
|
||||
|
||||
public UTorrentProxy(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_cookieContainer = new CookieContainer();
|
||||
}
|
||||
|
||||
public Int32 GetVersion(UTorrentSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<String, Object>();
|
||||
arguments.Add("action", "getsettings");
|
||||
|
||||
var result = ProcessRequest(arguments, settings);
|
||||
|
||||
return result.Build;
|
||||
}
|
||||
|
||||
public Dictionary<String, String> GetConfig(UTorrentSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<String, Object>();
|
||||
arguments.Add("action", "getsettings");
|
||||
|
||||
var result = ProcessRequest(arguments, settings);
|
||||
|
||||
var configuration = new Dictionary<String, String>();
|
||||
|
||||
foreach (var configItem in result.Settings)
|
||||
{
|
||||
configuration.Add(configItem[0].ToString(), configItem[2].ToString());
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public List<UTorrentTorrent> GetTorrents(UTorrentSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<String, Object>();
|
||||
arguments.Add("list", 1);
|
||||
|
||||
var result = ProcessRequest(arguments, settings);
|
||||
|
||||
return result.Torrents;
|
||||
}
|
||||
|
||||
public void AddTorrentFromUrl(String torrentUrl, UTorrentSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<String, Object>();
|
||||
arguments.Add("action", "add-url");
|
||||
arguments.Add("s", torrentUrl);
|
||||
|
||||
ProcessRequest(arguments, settings);
|
||||
}
|
||||
|
||||
public void AddTorrentFromFile(String fileName, Byte[] fileContent, UTorrentSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<String, Object>();
|
||||
arguments.Add("action", "add-file");
|
||||
arguments.Add("path", String.Empty);
|
||||
|
||||
var client = BuildClient(settings);
|
||||
|
||||
// add-file should use POST unlike all other methods which are GET
|
||||
var request = new RestRequest(Method.POST);
|
||||
request.RequestFormat = DataFormat.Json;
|
||||
request.Resource = "/gui/";
|
||||
request.AddParameter("token", _authToken, ParameterType.QueryString);
|
||||
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
request.AddParameter(argument.Key, argument.Value, ParameterType.QueryString);
|
||||
}
|
||||
|
||||
request.AddFile("torrent_file", fileContent, fileName, @"application/octet-stream");
|
||||
|
||||
ProcessRequest(request, client);
|
||||
}
|
||||
|
||||
public void SetTorrentSeedingConfiguration(String hash, TorrentSeedConfiguration seedConfiguration, UTorrentSettings settings)
|
||||
{
|
||||
var arguments = new List<KeyValuePair<String, Object>>();
|
||||
arguments.Add("action", "setprops");
|
||||
arguments.Add("hash", hash);
|
||||
|
||||
arguments.Add("s", "seed_override");
|
||||
arguments.Add("v", 1);
|
||||
|
||||
if (seedConfiguration.Ratio != null)
|
||||
{
|
||||
arguments.Add("s","seed_ratio");
|
||||
arguments.Add("v", Convert.ToInt32(seedConfiguration.Ratio.Value * 1000));
|
||||
}
|
||||
|
||||
if (seedConfiguration.SeedTime != null)
|
||||
{
|
||||
arguments.Add("s", "seed_time");
|
||||
arguments.Add("v", Convert.ToInt32(seedConfiguration.SeedTime.Value.TotalSeconds));
|
||||
}
|
||||
|
||||
ProcessRequest(arguments, settings);
|
||||
}
|
||||
|
||||
public void RemoveTorrent(String hash, Boolean removeData, UTorrentSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<String, Object>();
|
||||
|
||||
if (removeData)
|
||||
{
|
||||
arguments.Add("action", "removedata");
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments.Add("action", "remove");
|
||||
}
|
||||
|
||||
arguments.Add("hash", hash);
|
||||
|
||||
ProcessRequest(arguments, settings);
|
||||
}
|
||||
|
||||
public void SetTorrentLabel(String hash, String label, UTorrentSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<String, Object>();
|
||||
arguments.Add("action", "setprops");
|
||||
arguments.Add("hash", hash);
|
||||
|
||||
arguments.Add("s", "label");
|
||||
arguments.Add("v", label);
|
||||
|
||||
ProcessRequest(arguments, settings);
|
||||
}
|
||||
|
||||
public void MoveTorrentToTopInQueue(String hash, UTorrentSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<String, Object>();
|
||||
arguments.Add("action", "queuetop");
|
||||
arguments.Add("hash", hash);
|
||||
|
||||
ProcessRequest(arguments, settings);
|
||||
}
|
||||
|
||||
public UTorrentResponse ProcessRequest(IEnumerable<KeyValuePair<String, Object>> arguments, UTorrentSettings settings)
|
||||
{
|
||||
var client = BuildClient(settings);
|
||||
|
||||
var request = new RestRequest(Method.GET);
|
||||
request.RequestFormat = DataFormat.Json;
|
||||
request.Resource = "/gui/";
|
||||
request.AddParameter("token", _authToken, ParameterType.QueryString);
|
||||
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
request.AddParameter(argument.Key, argument.Value, ParameterType.QueryString);
|
||||
}
|
||||
|
||||
return ProcessRequest(request, client);
|
||||
}
|
||||
|
||||
private UTorrentResponse ProcessRequest(IRestRequest request, IRestClient client)
|
||||
{
|
||||
_logger.Debug("Url: {0}", client.BuildUri(request));
|
||||
var clientResponse = client.Execute(request);
|
||||
|
||||
if (clientResponse.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
// Token has expired. If the settings were incorrect or the API is disabled we'd have gotten an error 400 during GetAuthToken
|
||||
_logger.Debug("uTorrent authentication token error.");
|
||||
|
||||
_authToken = GetAuthToken(client);
|
||||
|
||||
request.Parameters.First(v => v.Name == "token").Value = _authToken;
|
||||
clientResponse = client.Execute(request);
|
||||
}
|
||||
else if (clientResponse.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate");
|
||||
}
|
||||
|
||||
var uTorrentResult = clientResponse.Read<UTorrentResponse>(client);
|
||||
|
||||
return uTorrentResult;
|
||||
}
|
||||
|
||||
private String GetAuthToken(IRestClient client)
|
||||
{
|
||||
var request = new RestRequest();
|
||||
request.RequestFormat = DataFormat.Json;
|
||||
request.Resource = "/gui/token.html";
|
||||
|
||||
_logger.Debug("Url: {0}", client.BuildUri(request));
|
||||
var response = client.Execute(request);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate");
|
||||
}
|
||||
|
||||
response.ValidateResponse(client);
|
||||
|
||||
var xmlDoc = new System.Xml.XmlDocument();
|
||||
xmlDoc.LoadXml(response.Content);
|
||||
|
||||
var authToken = xmlDoc.FirstChild.FirstChild.InnerText;
|
||||
|
||||
_logger.Debug("uTorrent AuthToken={0}", authToken);
|
||||
|
||||
return authToken;
|
||||
}
|
||||
|
||||
private IRestClient BuildClient(UTorrentSettings settings)
|
||||
{
|
||||
var url = String.Format(@"http://{0}:{1}",
|
||||
settings.Host,
|
||||
settings.Port);
|
||||
|
||||
var restClient = RestClientFactory.BuildClient(url);
|
||||
|
||||
restClient.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password);
|
||||
restClient.CookieContainer = _cookieContainer;
|
||||
|
||||
if (_authToken.IsNullOrWhiteSpace())
|
||||
{
|
||||
// µTorrent requires a token and cookie for authentication. The cookie is set automatically when getting the token.
|
||||
_authToken = GetAuthToken(restClient);
|
||||
}
|
||||
|
||||
return restClient;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
{
|
||||
public class UTorrentResponse
|
||||
{
|
||||
public Int32 Build { get; set; }
|
||||
public List<UTorrentTorrent> Torrents { get; set; }
|
||||
public List<String[]> Label { get; set; }
|
||||
public List<Object> RssFeeds { get; set; }
|
||||
public List<Object> RssFilters { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "torrentc")]
|
||||
public String CacheNumber { get; set; }
|
||||
|
||||
public List<Object[]> Settings { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
{
|
||||
public class UTorrentSettingsValidator : AbstractValidator<UTorrentSettings>
|
||||
{
|
||||
public UTorrentSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Host).NotEmpty();
|
||||
RuleFor(c => c.Port).InclusiveBetween(0, 65535);
|
||||
RuleFor(c => c.TvCategory).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class UTorrentSettings : IProviderConfig
|
||||
{
|
||||
private static readonly UTorrentSettingsValidator validator = new UTorrentSettingsValidator();
|
||||
|
||||
public UTorrentSettings()
|
||||
{
|
||||
Host = "localhost";
|
||||
Port = 9091;
|
||||
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 = "Username", Type = FieldType.Textbox)]
|
||||
public String Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
|
||||
public String Password { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox)]
|
||||
public String TvCategory { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
|
||||
public Int32 RecentTvPriority { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
|
||||
public Int32 OlderTvPriority { get; set; }
|
||||
|
||||
public ValidationResult Validate()
|
||||
{
|
||||
return validator.Validate(this);
|
||||
}
|
||||
}
|
||||
}
|
116
src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentTorrent.cs
Normal file
116
src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentTorrent.cs
Normal file
@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
{
|
||||
[JsonConverter(typeof(UTorrentTorrentJsonConverter))]
|
||||
public class UTorrentTorrent
|
||||
{
|
||||
public String Hash { get; set; }
|
||||
public UTorrentTorrentStatus Status { get; set; }
|
||||
public String Name { get; set; }
|
||||
public Int64 Size { get; set; }
|
||||
public Double Progress { get; set; }
|
||||
public Int64 Downloaded { get; set; }
|
||||
public Int64 Uploaded { get; set; }
|
||||
public Double Ratio { get; set; }
|
||||
public Int32 UploadSpeed { get; set; }
|
||||
public Int32 DownloadSpeed { get; set; }
|
||||
|
||||
public Int32 Eta { get; set; }
|
||||
public String Label { get; set; }
|
||||
public Int32 PeersConnected { get; set; }
|
||||
public Int32 PeersInSwarm { get; set; }
|
||||
public Int32 SeedsConnected { get; set; }
|
||||
public Int32 SeedsInSwarm { get; set; }
|
||||
public Double Availablity { get; set; }
|
||||
public Int32 TorrentQueueOrder { get; set; }
|
||||
public Int64 Remaining { get; set; }
|
||||
public String DownloadUrl { get; set; }
|
||||
|
||||
public Object RssFeedUrl { get; set; }
|
||||
public Object StatusMessage { get; set; }
|
||||
public Object StreamId { get; set; }
|
||||
public Object DateAdded { get; set; }
|
||||
public Object DateCompleted { get; set; }
|
||||
public Object AppUpdateUrl { get; set; }
|
||||
public String RootDownloadPath { get; set; }
|
||||
public Object Unknown27 { get; set; }
|
||||
public Object Unknown28 { get; set; }
|
||||
}
|
||||
|
||||
class UTorrentTorrentJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(UTorrentTorrent);
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var result = new UTorrentTorrent();
|
||||
|
||||
result.Hash = reader.ReadAsString();
|
||||
result.Status = (UTorrentTorrentStatus)reader.ReadAsInt32();
|
||||
result.Name = reader.ReadAsString();
|
||||
reader.Read();
|
||||
result.Size = (Int64)reader.Value;
|
||||
result.Progress = (int)reader.ReadAsInt32() / 1000.0;
|
||||
reader.Read();
|
||||
result.Downloaded = (Int64)reader.Value;
|
||||
reader.Read();
|
||||
result.Uploaded = (Int64)reader.Value;
|
||||
result.Ratio = (int)reader.ReadAsInt32() / 1000.0;
|
||||
result.UploadSpeed = (int)reader.ReadAsInt32();
|
||||
result.DownloadSpeed = (int)reader.ReadAsInt32();
|
||||
|
||||
result.Eta = (int)reader.ReadAsInt32();
|
||||
result.Label = reader.ReadAsString();
|
||||
result.PeersConnected = (int)reader.ReadAsInt32();
|
||||
result.PeersInSwarm = (int)reader.ReadAsInt32();
|
||||
result.SeedsConnected = (int)reader.ReadAsInt32();
|
||||
result.SeedsInSwarm = (int)reader.ReadAsInt32();
|
||||
result.Availablity = (int)reader.ReadAsInt32() / 65536.0;
|
||||
result.TorrentQueueOrder = (int)reader.ReadAsInt32();
|
||||
reader.Read();
|
||||
result.Remaining = (Int64)reader.Value;
|
||||
|
||||
reader.Read();
|
||||
|
||||
// Builds before 25406 don't return the remaining items.
|
||||
|
||||
if (reader.TokenType != JsonToken.EndArray)
|
||||
{
|
||||
result.DownloadUrl = (String)reader.Value;
|
||||
|
||||
reader.Read();
|
||||
result.RssFeedUrl = reader.Value;
|
||||
reader.Read();
|
||||
result.StatusMessage = reader.Value;
|
||||
reader.Read();
|
||||
result.StreamId = reader.Value;
|
||||
reader.Read();
|
||||
result.DateAdded = reader.Value;
|
||||
reader.Read();
|
||||
result.DateCompleted = reader.Value;
|
||||
reader.Read();
|
||||
result.AppUpdateUrl = reader.Value;
|
||||
result.RootDownloadPath = reader.ReadAsString();
|
||||
reader.Read();
|
||||
result.Unknown27 = reader.Value;
|
||||
reader.Read();
|
||||
result.Unknown28 = reader.Value;
|
||||
|
||||
while(reader.TokenType != JsonToken.EndArray)
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||
{
|
||||
[Flags]
|
||||
public enum UTorrentTorrentStatus
|
||||
{
|
||||
Started = 1,
|
||||
Checking = 2,
|
||||
StartAfterCheck = 4,
|
||||
Checked = 8,
|
||||
Error = 16,
|
||||
Paused = 32,
|
||||
Queued = 64,
|
||||
Loaded = 128
|
||||
}
|
||||
}
|
@ -135,7 +135,16 @@ private Boolean UpdateTrackedDownloads(List<History.History> grabbedHistory)
|
||||
|
||||
foreach (var downloadClient in downloadClients)
|
||||
{
|
||||
var downloadClientHistory = downloadClient.GetItems().ToList();
|
||||
List<DownloadClientItem> downloadClientHistory;
|
||||
try
|
||||
{
|
||||
downloadClientHistory = downloadClient.GetItems().ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.WarnException("Unable to retrieve queue and history items from " + downloadClient.Definition.Name, ex);
|
||||
continue;
|
||||
}
|
||||
foreach (var downloadItem in downloadClientHistory)
|
||||
{
|
||||
var trackingId = String.Format("{0}-{1}", downloadClient.Definition.Id, downloadItem.DownloadClientId);
|
||||
|
180
src/NzbDrone.Core/Download/TorrentClientBase.cs
Normal file
180
src/NzbDrone.Core/Download/TorrentClientBase.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using MonoTorrent;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NLog;
|
||||
using NzbDrone.Core.RemotePathMappings;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public abstract class TorrentClientBase<TSettings> : DownloadClientBase<TSettings>
|
||||
where TSettings : IProviderConfig, new()
|
||||
{
|
||||
protected readonly IHttpClient _httpClient;
|
||||
protected readonly ITorrentFileInfoReader _torrentFileInfoReader;
|
||||
|
||||
protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader,
|
||||
IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IParsingService parsingService,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
Logger logger)
|
||||
: base(configService, diskProvider, remotePathMappingService, logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_torrentFileInfoReader = torrentFileInfoReader;
|
||||
}
|
||||
|
||||
public override DownloadProtocol Protocol
|
||||
{
|
||||
get
|
||||
{
|
||||
return DownloadProtocol.Torrent;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract String AddFromMagnetLink(RemoteEpisode remoteEpisode, String hash, String magnetLink);
|
||||
protected abstract String AddFromTorrentFile(RemoteEpisode remoteEpisode, String hash, String filename, Byte[] fileContent);
|
||||
|
||||
public override String Download(RemoteEpisode remoteEpisode)
|
||||
{
|
||||
var torrentInfo = remoteEpisode.Release as TorrentInfo;
|
||||
|
||||
String magnetUrl = null;
|
||||
String torrentUrl = null;
|
||||
|
||||
if (remoteEpisode.Release.DownloadUrl.StartsWith("magnet:"))
|
||||
{
|
||||
magnetUrl = remoteEpisode.Release.DownloadUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
torrentUrl = remoteEpisode.Release.DownloadUrl;
|
||||
}
|
||||
|
||||
if (torrentInfo != null && !torrentInfo.MagnetUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
magnetUrl = torrentInfo.MagnetUrl;
|
||||
}
|
||||
|
||||
String hash = null;
|
||||
|
||||
if (!magnetUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
hash = DownloadFromMagnetUrl(remoteEpisode, magnetUrl);
|
||||
}
|
||||
|
||||
if (hash == null && !torrentUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
hash = DownloadFromWebUrl(remoteEpisode, torrentUrl);
|
||||
}
|
||||
|
||||
if (hash == null)
|
||||
{
|
||||
throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed");
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
private string DownloadFromWebUrl(RemoteEpisode remoteEpisode, String torrentUrl)
|
||||
{
|
||||
Byte[] torrentFile = null;
|
||||
|
||||
try
|
||||
{
|
||||
var request = new HttpRequest(torrentUrl);
|
||||
request.Headers.Accept = "application/x-bittorrent";
|
||||
request.AllowAutoRedirect = false;
|
||||
|
||||
var response = _httpClient.Get(request);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.SeeOther)
|
||||
{
|
||||
var locationHeader = (string)response.Headers.GetValueOrDefault("Location", null);
|
||||
|
||||
if (locationHeader != null && locationHeader.StartsWith("magnet:"))
|
||||
{
|
||||
return DownloadFromMagnetUrl(remoteEpisode, locationHeader);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new WebException("Remote website tried to redirect without providing a location.");
|
||||
}
|
||||
}
|
||||
|
||||
torrentFile = response.ResponseData;
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.ErrorException(String.Format("Downloading torrentfile for episode '{0}' failed ({1})",
|
||||
remoteEpisode.Release.Title, torrentUrl), ex);
|
||||
|
||||
throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed", ex);
|
||||
}
|
||||
|
||||
|
||||
var filename = String.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteEpisode.Release.Title));
|
||||
|
||||
var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile);
|
||||
|
||||
var actualHash = AddFromTorrentFile(remoteEpisode, hash, filename, torrentFile);
|
||||
|
||||
if (hash != actualHash)
|
||||
{
|
||||
_logger.Warn(
|
||||
"{0} did not return the expected InfoHash for '{1}', NzbDrone could potential lose track of the download in progress.",
|
||||
Definition.Implementation, remoteEpisode.Release.DownloadUrl);
|
||||
}
|
||||
|
||||
return actualHash;
|
||||
}
|
||||
|
||||
private String DownloadFromMagnetUrl(RemoteEpisode remoteEpisode, String magnetUrl)
|
||||
{
|
||||
String hash = null;
|
||||
String actualHash = null;
|
||||
|
||||
try
|
||||
{
|
||||
hash = new MagnetLink(magnetUrl).InfoHash.ToHex();
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
_logger.ErrorException(String.Format("Failed to parse magnetlink for episode '{0}': '{1}'",
|
||||
remoteEpisode.Release.Title, magnetUrl), ex);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hash != null)
|
||||
{
|
||||
actualHash = AddFromMagnetLink(remoteEpisode, hash, magnetUrl);
|
||||
}
|
||||
|
||||
if (hash != actualHash)
|
||||
{
|
||||
_logger.Warn(
|
||||
"{0} did not return the expected InfoHash for '{1}', NzbDrone could potential lose track of the download in progress.",
|
||||
Definition.Implementation, remoteEpisode.Release.DownloadUrl);
|
||||
}
|
||||
|
||||
return actualHash;
|
||||
}
|
||||
}
|
||||
}
|
35
src/NzbDrone.Core/Indexers/BitMeTv/BitMeTv.cs
Normal file
35
src/NzbDrone.Core/Indexers/BitMeTv/BitMeTv.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using FluentValidation.Results;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.BitMeTv
|
||||
{
|
||||
public class BitMeTv : HttpIndexerBase<BitMeTvSettings>
|
||||
{
|
||||
public override DownloadProtocol Protocol { get { return DownloadProtocol.Torrent; } }
|
||||
public override Boolean SupportsSearch { get { return false; } }
|
||||
public override Int32 PageSize { get { return 0; } }
|
||||
|
||||
public BitMeTv(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger)
|
||||
: base(httpClient, configService, parsingService, logger)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new BitMeTvRequestGenerator() { Settings = Settings };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new TorrentRssParser() { ParseSizeInDescription = true };
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.BitMeTv
|
||||
{
|
||||
public class BitMeTvRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public BitMeTvSettings Settings { get; set; }
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetRecentRequests()
|
||||
{
|
||||
var pageableRequests = new List<IEnumerable<IndexerRequest>>();
|
||||
|
||||
pageableRequests.AddIfNotNull(GetRssRequests(null));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IEnumerable<IndexerRequest>>();
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(SeasonSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IEnumerable<IndexerRequest>>();
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IEnumerable<IndexerRequest>>();
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IEnumerable<IndexerRequest>>();
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IEnumerable<IndexerRequest>>();
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRssRequests(String searchParameters)
|
||||
{
|
||||
yield return new IndexerRequest(String.Format("{0}/rss.php?uid={1}&passkey={2}{3}", Settings.BaseUrl.Trim().TrimEnd('/'), Settings.UserId, Settings.RssPasskey, searchParameters), HttpAccept.Html);
|
||||
}
|
||||
}
|
||||
}
|
44
src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvSettings.cs
Normal file
44
src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvSettings.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.BitMeTv
|
||||
{
|
||||
public class BitMeTvSettingsValidator : AbstractValidator<BitMeTvSettings>
|
||||
{
|
||||
public BitMeTvSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
RuleFor(c => c.UserId).NotEmpty();
|
||||
RuleFor(c => c.RssPasskey).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class BitMeTvSettings : IProviderConfig
|
||||
{
|
||||
private static readonly BitMeTvSettingsValidator validator = new BitMeTvSettingsValidator();
|
||||
|
||||
public BitMeTvSettings()
|
||||
{
|
||||
BaseUrl = "http://www.bitmetv.org";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Website URL")]
|
||||
public String BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "UserId")]
|
||||
public String UserId { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "RSS Passkey")]
|
||||
public String RssPasskey { get; set; }
|
||||
|
||||
public ValidationResult Validate()
|
||||
{
|
||||
return validator.Validate(this);
|
||||
}
|
||||
}
|
||||
}
|
39
src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNet.cs
Normal file
39
src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNet.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
{
|
||||
public class BroadcastheNet : HttpIndexerBase<BroadcastheNetSettings>
|
||||
{
|
||||
public override DownloadProtocol Protocol { get { return DownloadProtocol.Torrent; } }
|
||||
public override bool SupportsRss { get { return true; } }
|
||||
public override bool SupportsSearch { get { return true; } }
|
||||
public override int PageSize { get { return 100; } }
|
||||
|
||||
public BroadcastheNet(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger)
|
||||
: base(httpClient, configService, parsingService, logger)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new BroadcastheNetRequestGenerator() { Settings = Settings, PageSize = PageSize };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new BroadcastheNetParser();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
{
|
||||
public class BroadcastheNetParser : IParseIndexerResponse
|
||||
{
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var results = new List<ReleaseInfo>();
|
||||
|
||||
if (indexerResponse.HttpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new ApiKeyException("API Key invalid or not authorized");
|
||||
}
|
||||
else if (indexerResponse.HttpResponse.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned NotFound, the Indexer API may have changed.");
|
||||
}
|
||||
else if (indexerResponse.HttpResponse.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
|
||||
{
|
||||
throw new RequestLimitReachedException("Cannot do more than 150 API requests per hour.");
|
||||
}
|
||||
else if (indexerResponse.HttpResponse.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<JsonRpcResponse<BroadcastheNetTorrents>>(indexerResponse.HttpResponse).Resource;
|
||||
|
||||
if (jsonResponse.Error != null || jsonResponse.Result == null)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error);
|
||||
}
|
||||
|
||||
if (jsonResponse.Result.Results == 0)
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
foreach (var torrent in jsonResponse.Result.Torrents.Values)
|
||||
{
|
||||
var torrentInfo = new TorrentInfo();
|
||||
|
||||
torrentInfo.Guid = String.Format("BTN-{0}", torrent.TorrentID.ToString());
|
||||
torrentInfo.Title = torrent.ReleaseName;
|
||||
torrentInfo.Size = torrent.Size;
|
||||
torrentInfo.DownloadUrl = torrent.DownloadURL;
|
||||
torrentInfo.InfoUrl = String.Format("https://broadcasthe.net/torrents.php?id={0}&torrentid={1}", torrent.GroupID, torrent.TorrentID);
|
||||
//torrentInfo.CommentUrl =
|
||||
if (torrent.TvrageID.HasValue)
|
||||
{
|
||||
torrentInfo.TvRageId = torrent.TvrageID.Value;
|
||||
}
|
||||
torrentInfo.PublishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).ToUniversalTime().AddSeconds(torrent.Time);
|
||||
//torrentInfo.MagnetUrl =
|
||||
torrentInfo.InfoHash = torrent.InfoHash;
|
||||
torrentInfo.Seeds = torrent.Seeders;
|
||||
torrentInfo.Peers = torrent.Leechers;
|
||||
|
||||
results.Add(torrentInfo);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
{
|
||||
public class BroadcastheNetRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public Int32 MaxPages { get; set; }
|
||||
public Int32 PageSize { get; set; }
|
||||
public BroadcastheNetSettings Settings { get; set; }
|
||||
|
||||
public BroadcastheNetRequestGenerator()
|
||||
{
|
||||
MaxPages = 10;
|
||||
PageSize = 100;
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetRecentRequests()
|
||||
{
|
||||
var pageableRequests = new List<IEnumerable<IndexerRequest>>();
|
||||
|
||||
pageableRequests.AddIfNotNull(GetPagedRequests(1, null));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequest = new List<IEnumerable<IndexerRequest>>();
|
||||
|
||||
var parameters = new BroadcastheNetTorrentQuery();
|
||||
if (AddSeriesSearchParameters(parameters, searchCriteria))
|
||||
{
|
||||
parameters.Category = "Episode";
|
||||
parameters.Name = String.Format("S{0:00}E{1:00}", searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber);
|
||||
|
||||
pageableRequest.AddIfNotNull(GetPagedRequests(MaxPages, parameters));
|
||||
}
|
||||
|
||||
return pageableRequest;
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(SeasonSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequest = new List<IEnumerable<IndexerRequest>>();
|
||||
|
||||
var parameters = new BroadcastheNetTorrentQuery();
|
||||
if (AddSeriesSearchParameters(parameters, searchCriteria))
|
||||
{
|
||||
parameters.Category = "Episode";
|
||||
parameters.Name = String.Format("S{0:00}E%", searchCriteria.SeasonNumber);
|
||||
|
||||
pageableRequest.AddIfNotNull(GetPagedRequests(MaxPages, parameters));
|
||||
|
||||
parameters = parameters.Clone();
|
||||
|
||||
parameters.Category = "Season";
|
||||
parameters.Name = String.Format("Season {0}", searchCriteria.SeasonNumber);
|
||||
|
||||
pageableRequest.AddIfNotNull(GetPagedRequests(MaxPages, parameters));
|
||||
}
|
||||
|
||||
|
||||
return pageableRequest;
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequest = new List<IEnumerable<IndexerRequest>>();
|
||||
|
||||
var parameters = new BroadcastheNetTorrentQuery();
|
||||
if (AddSeriesSearchParameters(parameters, searchCriteria))
|
||||
{
|
||||
parameters.Category = "Episode";
|
||||
parameters.Name = String.Format("{0:yyyy}.{0:MM}.{0:dd}", searchCriteria.AirDate);
|
||||
|
||||
pageableRequest.AddIfNotNull(GetPagedRequests(MaxPages, parameters));
|
||||
}
|
||||
|
||||
return pageableRequest;
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IEnumerable<IndexerRequest>>();
|
||||
}
|
||||
|
||||
public virtual IList<IEnumerable<IndexerRequest>> GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
return new List<IEnumerable<IndexerRequest>>();
|
||||
}
|
||||
|
||||
private bool AddSeriesSearchParameters(BroadcastheNetTorrentQuery parameters, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
if (searchCriteria.Series.TvRageId != 0)
|
||||
{
|
||||
parameters.Tvrage = String.Format("{0}", searchCriteria.Series.TvRageId);
|
||||
return true;
|
||||
}
|
||||
else if (searchCriteria.Series.TvdbId != 0)
|
||||
{
|
||||
parameters.Tvdb = String.Format("{0}", searchCriteria.Series.TvdbId);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// BTN is very neatly managed, so it's unlikely they map tvrage/tvdb wrongly.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(Int32 maxPages, BroadcastheNetTorrentQuery parameters)
|
||||
{
|
||||
if (parameters == null)
|
||||
{
|
||||
parameters = new BroadcastheNetTorrentQuery();
|
||||
}
|
||||
|
||||
var builder = new JsonRpcRequestBuilder(Settings.BaseUrl, "getTorrents", new Object[] { Settings.ApiKey, parameters, PageSize, 0 });
|
||||
builder.SupressHttpError = true;
|
||||
|
||||
for (var page = 0; page < maxPages;page++)
|
||||
{
|
||||
builder.Parameters[3] = page * PageSize;
|
||||
|
||||
yield return new IndexerRequest(builder.Build(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
{
|
||||
public class BroadcastheNetSettingsValidator : AbstractValidator<BroadcastheNetSettings>
|
||||
{
|
||||
public BroadcastheNetSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class BroadcastheNetSettings : IProviderConfig
|
||||
{
|
||||
private static readonly BroadcastheNetSettingsValidator validator = new BroadcastheNetSettingsValidator();
|
||||
|
||||
public BroadcastheNetSettings()
|
||||
{
|
||||
BaseUrl = "http://api.btnapps.net/";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
|
||||
public String BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "API Key")]
|
||||
public String ApiKey { get; set; }
|
||||
|
||||
public ValidationResult Validate()
|
||||
{
|
||||
return validator.Validate(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
{
|
||||
public class BroadcastheNetTorrent
|
||||
{
|
||||
public String GroupName { get; set; }
|
||||
public Int32 GroupID { get; set; }
|
||||
public Int32 TorrentID { get; set; }
|
||||
public Int32 SeriesID { get; set; }
|
||||
public String Series { get; set; }
|
||||
public String SeriesBanner { get; set; }
|
||||
public String SeriesPoster { get; set; }
|
||||
public String YoutubeTrailer { get; set; }
|
||||
public String Category { get; set; }
|
||||
public Int32? Snatched { get; set; }
|
||||
public Int32? Seeders { get; set; }
|
||||
public Int32? Leechers { get; set; }
|
||||
public String Source { get; set; }
|
||||
public String Container { get; set; }
|
||||
public String Codec { get; set; }
|
||||
public String Resolution { get; set; }
|
||||
public String Origin { get; set; }
|
||||
public String ReleaseName { get; set; }
|
||||
public Int64 Size { get; set; }
|
||||
public Int64 Time { get; set; }
|
||||
public Int32? TvdbID { get; set; }
|
||||
public Int32? TvrageID { get; set; }
|
||||
public String ImdbID { get; set; }
|
||||
public String InfoHash { get; set; }
|
||||
public String DownloadURL { get; set; }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user