/*
* This file is part of Transdroid Torrent Search
* <http://code.google.com/p/transdroid-search/>
*
* Transdroid Torrent Search is free software: you can redistribute
* it and/or modify it under the terms of the GNU Lesser General
* Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later
* version.
*
* Transdroid Torrent Search is distributed in the hope that it will
* be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Transdroid. If not, see <http://www.gnu.org/licenses/>.
*/
package org.transdroid.search.hdbitsorg;
import java.io.InputStream;
import java.net.URLEncoder;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.security.auth.login.LoginException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.transdroid.search.ISearchAdapter;
import org.transdroid.search.SearchResult;
import org.transdroid.search.SortOrder;
import org.transdroid.search.TorrentSite;
import org.transdroid.search.gui.SettingsHelper;
import org.transdroid.util.HttpHelper;
import android.content.SharedPreferences;
import android.util.Log;
/**
* An adapter that provides access to hdbits.org searches by parsing the raw HTML output.
*
* @author John Conrad
*/
public class HdBitsOrgAdapter implements ISearchAdapter {
private static final String LOG_TAG = HdBitsOrgAdapter.class.getName();
private static final String LOGIN_FORM_URL = "https://hdbits.org/login";
private static final String LOGIN_URL = "https://hdbits.org/login/doLogin";
private static final String LOGIN_TOKEN_REGEX = "<input[^>]*name=\"lol\"[^>]*value=\"([^\"]+)\"[^>]*>";
// without escapes <input[^>]*name="lol"[^>]*value="([^"]+)"[^>]*>
private static final String LOGIN_POST_USERNAME = "uname";
private static final String LOGIN_POST_PASSWORD = "password";
private static final String LOGIN_POST_TOKEN = "lol";
private static final String SEARCH_URL = "http://hdbits.org/browse.php?search=%1$s";
private static final String SEARCH_SORT_BY_SEEDERS_SUFFIX = "&sort=seeders&d=DESC";
private static final String SEARCH_REGEX = "<tr id='t[^']+'[^>]*>.*?href=\"/(details.php?[^\"]+)\"[^>]*?>([^<]*?)<.*?href=\"(download.php/[^\"]*?)\".*?<td[^>]*>(\\d*)\\W(day|month).*?<br />(\\d*)\\W*(day|hour).*?>([^<]*)<br>(GB|MB).*?toseeders=1\"><[^>]*>([^<]*).*?<td[^>]+.*?>(\\d+)";
// without escapes: <tr id='t[^']+'[^>]*>.*?href="/(details.php?[^"]+)"[^>]*?>([^<]*?)<.*?href="(download.php/[^"]*?)".*?<td[^>]*>(\d*)\W(day|month).*?<br />(\d*)\W*(day|hour).*?>([^<]*)<br>(GB|MB).*?toseeders=1"><[^>]*>([^<]*).*?<td[^>]+.*?>(\d+)
private static final String URL_PREFIX = "https://hdbits.org/";
private static final int CONNECTION_TIMEOUT = 8000;
// =========================================================
// ISearchAdapter
// =========================================================
@Override
public String getSiteName() {
return "hdbits.org";
}
@Override
public boolean isPrivateSite() {
return true;
}
@Override
public boolean usesToken() {
return false;
}
@Override
public List<SearchResult> search(SharedPreferences prefs, String query, SortOrder order, int maxResults) throws Exception {
DefaultHttpClient client = prepareRequest(prefs);
// build search query
String encodedQuery = URLEncoder.encode(query, "UTF-8");
String url = String.format(SEARCH_URL, encodedQuery);
if (order == SortOrder.BySeeders) url += SEARCH_SORT_BY_SEEDERS_SUFFIX;
// make request
Log.d(LOG_TAG, "Executing search request from: " + url);
HttpResponse response = client.execute(new HttpGet(url));
// parse HTML response into a list of torrents
String html = HttpHelper.convertStreamToString(response.getEntity().getContent());
return parseHtml(html, maxResults);
}
@Override
public String buildRssFeedUrlFromSearch(String query, SortOrder order) {
// no rss based search for hdbits. there is a live RSS feed for all
// torrents on the site but it does not provide search capability
return null;
}
@Override
public InputStream getTorrentFile(SharedPreferences prefs, String url) throws Exception {
// Provide an authenticated file handle to the requested url
DefaultHttpClient client = prepareRequest(prefs);
HttpResponse response = client.execute(new HttpGet(url));
return response.getEntity().getContent();
}
// =========================================================
// LOGIN LOGIC
// =========================================================
private DefaultHttpClient prepareRequest(SharedPreferences prefs) throws Exception {
Log.d(LOG_TAG, "Preparing login attempt.");
// retrieve stored login info
String username = SettingsHelper.getSiteUser(prefs, TorrentSite.HdBitsOrg);
String password = SettingsHelper.getSitePass(prefs, TorrentSite.HdBitsOrg);
// verify we have login credentials. does this ever get hit?
if (username == null || password == null) {
throw new InvalidParameterException(
"No username or password was provided, while this is required for this private site.");
}
// setup our http client
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
HttpConnectionParams.setSoTimeout(params, CONNECTION_TIMEOUT);
DefaultHttpClient client = new DefaultHttpClient(params);
// grab our unique login token
String token = grabToken(client);
// login to hdbits populating the HttpClient with the required cookies
login(client, username, password, token);
return client;
}
/**
* Retrieves a hidden token from the hdbits.org login form.
*/
private String grabToken(DefaultHttpClient client) throws Exception {
Log.d(LOG_TAG, "Retrieving login token.");
// grab html
HttpResponse response = client.execute(new HttpGet(LOGIN_FORM_URL));
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
throw new Exception("Failed to retrieve hdbits.org login form.");
// try to find the hidden parameter on the login form
String html = HttpHelper.convertStreamToString(response.getEntity().getContent());
Pattern tokenRegexParser = Pattern.compile(LOGIN_TOKEN_REGEX);
Matcher match = tokenRegexParser.matcher(html);
boolean success = match.find();
if (!success) throw new Exception("Unable to find hdbits.org login token. Has website HTML changed?");
// success!
return match.group(1);
}
/**
* Attempts to log in to hdbits.org with the given credentials. On success
* the given DefaultHttpClient should hold all required cookies to access
* the site.
*/
private void login(DefaultHttpClient client, String username, String password, String token) throws Exception {
Log.d(LOG_TAG, "Attempting to login.");
HttpPost request = new HttpPost(LOGIN_URL);
request.setEntity(new UrlEncodedFormEntity(Arrays
.asList(new BasicNameValuePair[] {
new BasicNameValuePair(LOGIN_POST_USERNAME, username),
new BasicNameValuePair(LOGIN_POST_PASSWORD, password),
new BasicNameValuePair(LOGIN_POST_TOKEN, token),
new BasicNameValuePair("returnto", "%2F") })));
client.execute(request);
// verify we have the cookies needed to log in
boolean success = false, uid = false, pass = false, hash = false;
for (Cookie cookie : client.getCookieStore().getCookies()) {
if ("uid".equals(cookie.getName())) uid = true;
if ("pass".equals(cookie.getName())) pass = true;
if ("hash".equals(cookie.getName())) hash = true;
}
// if we don't have the correct cookies, login failed. notify user with a toast and toss an exception.
success = uid && pass && hash;
if (!success) {
Log.e(LOG_TAG, "Failed to log into hdbits.org as '" + username + "'. Did not receive expected login cookies!");
throw new LoginException("Failed to log into hdbits.org as '" + username + "'. Did not receive expected login cookies!");
}
Log.d(LOG_TAG, "Successfully logged in to hdbits.org.");
}
// =========================================================
// SEARCH LOGIC
// =========================================================
protected List<SearchResult> parseHtml(String html, int maxResults) throws Exception {
Log.d(LOG_TAG, "Parsing search results.");
List<SearchResult> results = new ArrayList<SearchResult>();
int matchCount = 0;
int errorCount = 0;
Pattern regex = Pattern.compile(SEARCH_REGEX, Pattern.DOTALL);
Matcher match = regex.matcher(html);
while (match.find() && matchCount < maxResults) {
matchCount++;
if (match.groupCount() != 11) {
errorCount++;
continue;
}
String detailsUrl = URL_PREFIX + match.group(1);
String title = match.group(2);
String torrentUrl = URL_PREFIX + match.group(3);
String size = match.group(8) + match.group(9); // size + unit
int seeders = Integer.parseInt(match.group(10));
int leechers = Integer.parseInt(match.group(11));
int time1 = Integer.parseInt(match.group(4));
String timeUnit1 = match.group(5);
int time2 = Integer.parseInt(match.group(6));
String timeUnit2 = match.group(7);
// hdbits.org lists "added date" in a relative format (i.e. 8 months 7 days ago)
// we roughly calculate the number of MS elapsed then subtract that from "now"
// could be a day or two off depending on month lengths, it's just imprecise data
long elapsedTime = 0;
if (timeUnit1.startsWith("month")) elapsedTime += time1 * 1000L * 60L * 60L * 24L * 30L;
if (timeUnit1.startsWith("day")) elapsedTime += time1 * 1000L * 60L * 60L * 24L;
if (timeUnit2.startsWith("day")) elapsedTime += time2 * 1000L * 60L * 60L * 24L;
if (timeUnit2.startsWith("hour")) elapsedTime += time2 * 1000L * 60L * 60L;
Date addedDate = new Date();
addedDate.setTime(addedDate.getTime() - elapsedTime);
// build our search result
SearchResult torrent = new SearchResult(title, torrentUrl, detailsUrl, size, addedDate, seeders, leechers);
results.add(torrent);
}
Log.d(LOG_TAG, "Found " + matchCount + " matches and successfully parsed " + (matchCount - errorCount) + " of those matches.");
return results;
}
}