using System; using System.Net; using System.Linq; using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Core.Rest; using NLog; using RestSharp; using Newtonsoft.Json.Linq; namespace NzbDrone.Core.Download.Clients.Transmission { public interface ITransmissionProxy { List 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 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 GetTorrents(TransmissionSettings settings) { var result = GetTorrentStatus(settings); var torrents = ((JArray)result.Arguments["torrents"]).ToObject>(); return torrents; } public void AddTorrentFromUrl(String torrentUrl, String downloadDirectory, TransmissionSettings settings) { var arguments = new Dictionary(); 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(); 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(); 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 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(); 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(); 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 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(); 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. switch (restResponse.StatusCode) { case 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; } case 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(); 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(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"; var url = String.Format(@"{0}://{1}:{2}/{3}/rpc", protocol, settings.Host, settings.Port, settings.UrlBase.Trim('/')); var restClient = RestClientFactory.BuildClient(url); restClient.FollowRedirects = false; if (!settings.Username.IsNullOrWhiteSpace()) { restClient.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password); } return restClient; } } }