added support for RSS feeds

This commit is contained in:
Arvid Norberg 2011-01-18 03:41:54 +00:00
parent 7b4998213f
commit c223291fb4
31 changed files with 2245 additions and 236 deletions

View File

@ -36,6 +36,7 @@ set(sources
piece_picker piece_picker
policy policy
puff puff
rss
session session
session_impl session_impl
settings settings

View File

@ -1,3 +1,4 @@
* added support for RSS feeds
* fixed up some edge cases in DHT routing table and improved unit test of it * fixed up some edge cases in DHT routing table and improved unit test of it
* added error category and error codes for HTTP errors * added error category and error codes for HTTP errors
* made the DHT implementation slightly more robust against routing table poisoning and node ID spoofing * made the DHT implementation slightly more robust against routing table poisoning and node ID spoofing

View File

@ -397,6 +397,7 @@ SOURCES =
piece_picker piece_picker
policy policy
puff puff
rss
session session
session_impl session_impl
settings settings

View File

@ -3,6 +3,7 @@ EXTRA_DIST = \
Jamfile \ Jamfile \
setup.py \ setup.py \
client.py \ client.py \
rss_reader.py \
simple_client.py \ simple_client.py \
src/alert.cpp \ src/alert.cpp \
src/big_number.cpp \ src/big_number.cpp \

View File

@ -0,0 +1,35 @@
#!/usr/bin/python
import sys
import libtorrent as lt
import time
if len(sys.argv) != 2:
print('usage: rss_reader.py rss-feed-url')
sys.exit(1)
ses = lt.session()
h = ses.add_feed({'url': sys.argv[1], 'auto_download': False})
f = h.get_feed_status()
spinner = ['|', '/', '-', '\\']
i = 0
while f['updating']:
time.sleep(0.1)
i = (i + 1) % 4
print('\b%s' % spinner[i]),
sys.stdout.flush()
f = h.get_feed_status()
print('\n\nFEED: %s' % f['url'])
if len(f['error']) > 0:
print('ERROR: %s' % f['error'])
print(' %s\n %s\n' % (f['title'], f['description']))
print(' ttl: %d minutes' % f['ttl'])
for item in f['items']:
print('\n%s\n------------------------------------------------------' % item['title'])
print(' url: %s\n size: %d\n uuid: %s\n description: %s' % (item['url'], item['size'], item['uuid'], item['description']))
print(' comment: %s\n category: %s' % (item['comment'], item['category']))

View File

@ -140,10 +140,8 @@ namespace
#endif #endif
#endif #endif
torrent_handle add_torrent(session& s, dict params) void dict_to_add_torrent_params(dict params, add_torrent_params& p)
{ {
add_torrent_params p;
if (params.has_key("ti")) if (params.has_key("ti"))
p.ti = new torrent_info(extract<torrent_info const&>(params["ti"])); p.ti = new torrent_info(extract<torrent_info const&>(params["ti"]));
@ -191,6 +189,12 @@ namespace
p.trackerid = extract<std::string>(params["trackerid"]); p.trackerid = extract<std::string>(params["trackerid"]);
if (params.has_key("url")) if (params.has_key("url"))
p.url = extract<std::string>(params["url"]); p.url = extract<std::string>(params["url"]);
}
torrent_handle add_torrent(session& s, dict params)
{
add_torrent_params p;
dict_to_add_torrent_params(params, p);
#ifndef BOOST_NO_EXCEPTIONS #ifndef BOOST_NO_EXCEPTIONS
return s.add_torrent(p); return s.add_torrent(p);
@ -200,6 +204,76 @@ namespace
#endif #endif
} }
void dict_to_feed_settings(dict params, feed_settings& feed)
{
if (params.has_key("auto_download"))
feed.auto_download = extract<bool>(params["auto_download"]);
if (params.has_key("default_ttl"))
feed.default_ttl = extract<int>(params["default_ttl"]);
if (params.has_key("url"))
feed.url = extract<std::string>(params["url"]);
if (params.has_key("add_args"))
dict_to_add_torrent_params(dict(params["add_args"]), feed.add_args);
}
feed_handle add_feed(session& s, dict params)
{
feed_settings feed;
dict_to_feed_settings(params, feed);
return s.add_feed(feed);
}
dict get_feed_status(feed_handle const& h)
{
feed_status s = h.get_feed_status();
dict ret;
ret["url"] = s.url;
ret["title"] = s.title;
ret["description"] = s.description;
ret["last_update"] = s.last_update;
ret["next_update"] = s.next_update;
ret["updating"] = s.updating;
ret["error"] = s.error ? s.error.message() : "";
ret["ttl"] = s.ttl;
list items;
for (std::vector<feed_item>::iterator i = s.items.begin()
, end(s.items.end()); i != end; ++i)
{
dict item;
item["url"] = i->url;
item["uuid"] = i->uuid;
item["title"] = i->title;
item["description"] = i->description;
item["comment"] = i->comment;
item["category"] = i->category;
item["size"] = i->size;
item["handle"] = i->handle;
item["info_hash"] = i->info_hash.to_string();
items.append(item);
}
ret["items"] = items;
return ret;
}
void set_feed_settings(feed_handle& h, dict sett)
{
feed_settings feed;
dict_to_feed_settings(sett, feed);
h.set_settings(feed);
}
dict get_feed_settings(feed_handle& h)
{
feed_settings s = h.settings();
dict ret;
ret["url"] = s.url;
ret["auto_download"] = s.auto_download;
ret["default_ttl"] = s.default_ttl;
return ret;
}
void start_natpmp(session& s) void start_natpmp(session& s)
{ {
allow_threading_guard guard; allow_threading_guard guard;
@ -399,6 +473,7 @@ void bind_session()
) )
#endif #endif
#endif #endif
.def("add_feed", &add_feed)
.def("remove_torrent", allow_threads(&session::remove_torrent), arg("option") = session::none .def("remove_torrent", allow_threads(&session::remove_torrent), arg("option") = session::none
) )
.def("set_local_download_rate_limit", allow_threads(&session::set_local_download_rate_limit)) .def("set_local_download_rate_limit", allow_threads(&session::set_local_download_rate_limit))
@ -484,5 +559,13 @@ void bind_session()
.value("save_as_map", session::save_as_map) .value("save_as_map", session::save_as_map)
; ;
class_<feed_handle>("feed_handle")
.def("update_feed", &feed_handle::update_feed)
.def("get_feed_status", &get_feed_status)
.def("set_settings", &set_feed_settings)
.def("settings", &get_feed_settings)
;
register_ptr_to_python<std::auto_ptr<alert> >(); register_ptr_to_python<std::auto_ptr<alert> >();
} }

File diff suppressed because it is too large Load Diff

View File

@ -126,6 +126,7 @@ The ``session`` class has the following synopsis::
save_i2p_proxy = 0x010, save_i2p_proxy = 0x010,
save_encryption_settings = 0x020, save_encryption_settings = 0x020,
save_as_map = 0x040, save_as_map = 0x040,
save_feeds = 0x080,
}; };
void load_state(lazy_entry const& e); void load_state(lazy_entry const& e);
@ -212,6 +213,10 @@ The ``session`` class has the following synopsis::
size_t set_alert_queue_size_limit( size_t set_alert_queue_size_limit(
size_t queue_size_limit_); size_t queue_size_limit_);
feed_handle session::add_feed(feed_settings const& feed);
void session::remove_feed(feed_handle h);
void session::get_feeds(std::vector<feed_handle>& f) const;
void add_extension(boost::function< void add_extension(boost::function<
boost::shared_ptr<torrent_plugin>(torrent*)> ext); boost::shared_ptr<torrent_plugin>(torrent*)> ext);
@ -309,6 +314,7 @@ torrents). These are the possible flags. A flag that's set, means those settings
save_i2p_proxy = 0x010, save_i2p_proxy = 0x010,
save_encryption_settings = 0x020, save_encryption_settings = 0x020,
save_as_map = 0x040, save_as_map = 0x040,
save_feeds = 0x080
}; };
@ -965,6 +971,78 @@ by calling ``pop_alert``. Default value is 1000.
``save_resume_data_alert`` and ``save_resume_data_failed_alert`` are always posted, regardelss ``save_resume_data_alert`` and ``save_resume_data_failed_alert`` are always posted, regardelss
of the alert mask. of the alert mask.
add_feed()
----------
::
feed_handle session::add_feed(feed_settings const& feed);
This adds an RSS feed to the session. The feed will be refreshed
regularly and optionally add all torrents from the feed, as they
appear. The feed is defined by the ``feed_settings`` object::
struct feed_settings
{
feed_settings();
std::string url;
bool auto_download;
int default_ttl;
add_torrent_params add_args;
};
By default ``auto_download`` is true, which means all torrents in
the feed will be downloaded. Set this to false in order to manually
add torrents to the session. You may react to the rss_alert_ when
a feed has been updated to poll it for the new items in the feed
when adding torrents manually. When torrents are added automatically,
you have to call ``session::get_torrents()`` to get the handles to
the new torrents.
Before adding the feed, you must set the ``url`` field to the
feed's url. It may point to an RSS or an atom feed.
The ``default_ttl`` is the default interval for refreshing a feed.
This may be overridden by the feed itself (by specifying the ``<ttl>``
tag) and defaults to 30 minutes. The field specifies the number of
minutes between refreshes.
If torrents are added automatically, you may want to set the
``add_args`` to appropriate values for download directory etc.
This object is used as a template for adding torrents from feeds,
but some torrent specific fields will be overridden by the
individual torrent being added. For more information on the
``add_torrent_params``, see `add_torrent()`_.
The returned feed_handle_ is a handle which is used to interact
with the feed, things like forcing a refresh or querying for
information about the items in the feed. For more information,
see feed_handle_.
remove_feed()
-------------
::
void session::remove_feed(feed_handle h);
Removes a feed from being watched by the session. When this
call returns, the feed handle is invalid and won't refer
to any feed.
get_feeds()
-----------
::
void session::get_feeds(std::vector<feed_handle>& f) const;
Returns a list of all RSS feeds that are being watched by the session.
add_extension() add_extension()
--------------- ---------------
@ -3772,6 +3850,127 @@ floating point operations are diabled, instead use ``progress_ppm``.
address of the interface it's going out over. This may be useful for multi-homed address of the interface it's going out over. This may be useful for multi-homed
clients with multiple interfaces to the internet. clients with multiple interfaces to the internet.
feed_handle
===========
The ``feed_handle`` refers to a specific RSS feed which is watched by the session.
The ``feed_item`` struct is defined in ``<libtorrent/rss.hpp>``. It has the following
functions::
struct feed_handle
{
feed_handle();
void update_feed();
feed_status get_feed_status() const;
void set_settings(feed_settings const& s);
feed_settings settings() const;
};
update_feed()
-------------
::
void update_feed();
Forces an update/refresh of the feed. Regular updates of the feed is managed
by libtorrent, be careful to not call this too frequently since it may
overload the RSS server.
get_feed_status()
-----------------
::
feed_status get_feed_status() const;
Queries the RSS feed for information, including all the items in the feed.
The ``feed_status`` object has the following fields::
struct feed_status
{
std::string url;
std::string title;
std::string description;
time_t last_update;
int next_update;
bool updating;
std::vector<feed_item> items;
error_code error;
int ttl;
};
``url`` is the URL of the feed.
``title`` is the name of the feed (as specified by the feed itself). This
may be empty if we have not recevied a response from the RSS server yet,
or if the feed does not specify a title.
``description`` is the feed description (as specified by the feed itself).
This may be empty if we have not received a response from the RSS server
yet, or if the feed does not specify a description.
``last_update`` is the posix time of the last successful response from the feed.
``next_update`` is the number of seconds, from now, when the feed will be
updated again.
``updating`` is true if the feed is currently being updated (i.e. waiting for
DNS resolution, connecting to the server or waiting for the response to the
HTTP request, or receiving the response).
``items`` is a vector of all items that we have received from the feed. See
feed_item_ for more information.
``error`` is set to the appropriate error code if the feed encountered an
error.
``ttl`` is the current refresh time (in minutes). It's either the configured
default ttl, or the ttl specified by the feed.
set_settings() settings()
-------------------------
::
void set_settings(feed_settings const& s);
feed_settings settings() const;
Sets and gets settings for this feed. For more information on the
available settings, see `add_feed()`_.
feed_item
=========
The ``feed_item`` struct is defined in ``<libtorrent/rss.hpp>``.
::
struct feed_item
{
feed_item();
std::string url;
std::string uuid;
std::string title;
std::string description;
std::string comment;
std::string category;
size_type size;
torrent_handle handle;
sha1_hash info_hash;
};
``size`` is the total size of the content the torrent refers to, or -1
if no size was specified by the feed.
``handle`` is the handle to the torrent, if the session is already downloading
this torrent.
``info_hash`` is the info-hash of the torrent, or cleared (i.e. all zeroes) if
the feed does not specify the info-hash.
All the strings are self explanatory and may be empty if the feed does not specify
those fields.
session customization session customization
===================== =====================
@ -5541,6 +5740,10 @@ is a bitmask with the following bits:
| ``dht_notification`` | Alerts on events in the DHT node. For incoming searches or | | ``dht_notification`` | Alerts on events in the DHT node. For incoming searches or |
| | bootstrapping being done etc. | | | bootstrapping being done etc. |
+--------------------------------+---------------------------------------------------------------------+ +--------------------------------+---------------------------------------------------------------------+
| ``rss_notification`` | Alerts on RSS related events, like feeds being updated, feed error |
| | conditions and successful RSS feed updates. Enabling this categoty |
| | will make you receive ``rss_alert`` alerts. |
+--------------------------------+---------------------------------------------------------------------+
| ``all_categories`` | The full bitmask, representing all available categories. | | ``all_categories`` | The full bitmask, representing all available categories. |
+--------------------------------+---------------------------------------------------------------------+ +--------------------------------+---------------------------------------------------------------------+
@ -6660,6 +6863,56 @@ when in anonymous mode.
communication and the tracker will not be contacted. The tracker which communication and the tracker will not be contacted. The tracker which
this failed for is specified in the ``str`` member. this failed for is specified in the ``str`` member.
rss_alert
---------
This alert is posted on RSS feed events such as start of RSS feed updates,
successful completed updates and errors during updates.
This alert is only posted if the ``rss_notifications`` category is enabled
in the alert mask.
::
struct rss_alert: alert
{
// ..
virtual std::string message() const;
enum state_t
{
state_updating, state_updated, state_error
};
feed_handle handle;
std::string url;
int state;
error_code error;
};
``handle`` is the handle to the feed which generated this alert.
``url`` is a short cut to access the url of the feed, without
having to call ``get_settings()``.
``state`` is one of:
``rss_alert::state_updating``
An update of this feed was just initiated, it will either succeed
or fail soon.
``rss_alert::state_updated``
The feed just completed a successful update, there may be new items
in it. If you're adding torrents manually, you may want to request
the feed status of the feed and look through the ``items`` vector.
``rss_akert::state_error``
An error just occurred. See the ``error`` field for information on
what went wrong.
``error`` is an error code used for when an error occurs on the feed.
alert dispatcher alert dispatcher
================ ================

View File

@ -141,6 +141,12 @@ a list of entries.</p>
</ul> </ul>
<p><tt class="docutils literal"><span class="pre">create_torrent::add_node()</span></tt> takes two arguments, one string and one integer, <p><tt class="docutils literal"><span class="pre">create_torrent::add_node()</span></tt> takes two arguments, one string and one integer,
instead of a pair. The string is the address and the integer is the port.</p> instead of a pair. The string is the address and the integer is the port.</p>
<p><tt class="docutils literal"><span class="pre">session::set_settings()</span></tt> not only accepts a <tt class="docutils literal"><span class="pre">session_settings</span></tt> object, but also
a dictionary with keys matching the names of the members of the <tt class="docutils literal"><span class="pre">session_settings</span></tt> struct.
When calling <tt class="docutils literal"><span class="pre">set_settings</span></tt>, the dictionary does not need to have every settings set,
keys that are not present, are set to their default value.</p>
<p>For backwards compatibility, <tt class="docutils literal"><span class="pre">session::settings()</span></tt> still returns a <tt class="docutils literal"><span class="pre">session_settings</span></tt>
struct. To get a python dictionary of the settings, call <tt class="docutils literal"><span class="pre">session::get_settings</span></tt>.</p>
<p>For an example python program, see <tt class="docutils literal"><span class="pre">client.py</span></tt> in the <tt class="docutils literal"><span class="pre">bindings/python</span></tt> <p>For an example python program, see <tt class="docutils literal"><span class="pre">client.py</span></tt> in the <tt class="docutils literal"><span class="pre">bindings/python</span></tt>
directory.</p> directory.</p>
<p>A very simple example usage of the module would be something like this:</p> <p>A very simple example usage of the module would be something like this:</p>

View File

@ -24,5 +24,6 @@ exe make_torrent : make_torrent.cpp ;
exe enum_if : enum_if.cpp ; exe enum_if : enum_if.cpp ;
exe connection_tester : connection_tester.cpp ; exe connection_tester : connection_tester.cpp ;
exe fragmentation_test : fragmentation_test.cpp ; exe fragmentation_test : fragmentation_test.cpp ;
exe rss_reader : rss_reader.cpp ;
exe upnp_test : upnp_test.cpp ; exe upnp_test : upnp_test.cpp ;

View File

@ -4,6 +4,7 @@ example_programs = \
enum_if \ enum_if \
make_torrent \ make_torrent \
simple_client \ simple_client \
rss_reader \
utp_test utp_test
if ENABLE_EXAMPLES if ENABLE_EXAMPLES
@ -28,6 +29,12 @@ simple_client_SOURCES = simple_client.cpp
enum_if_SOURCES = enum_if.cpp enum_if_SOURCES = enum_if.cpp
#enum_if_LDADD = $(top_builddir)/src/libtorrent-rasterbar.la #enum_if_LDADD = $(top_builddir)/src/libtorrent-rasterbar.la
rss_reader_SOURCES = rss_reader.cpp
#rss_reader_LDADD = $(top_builddir)/src/libtorrent-rasterbar.la
utp_test_SOURCES = rss_reader.cpp
#utp_test_LDADD = $(top_builddir)/src/libtorrent-rasterbar.la
LDADD = $(top_builddir)/src/libtorrent-rasterbar.la LDADD = $(top_builddir)/src/libtorrent-rasterbar.la
AM_CPPFLAGS = -ftemplate-depth-50 -I$(top_srcdir)/include @DEBUGFLAGS@ AM_CPPFLAGS = -ftemplate-depth-50 -I$(top_srcdir)/include @DEBUGFLAGS@

173
examples/rss_reader.cpp Normal file
View File

@ -0,0 +1,173 @@
#include "libtorrent/rss.hpp"
#include "libtorrent/session.hpp"
#include "libtorrent/bencode.hpp"
using namespace libtorrent;
void print_feed(feed_status const& f)
{
printf("FEED: %s\n",f.url.c_str());
if (f.error)
printf("ERROR: %s\n", f.error.message().c_str());
printf(" %s\n %s\n", f.title.c_str(), f.description.c_str());
printf(" ttl: %d minutes\n", f.ttl);
for (std::vector<feed_item>::const_iterator i = f.items.begin()
, end(f.items.end()); i != end; ++i)
{
printf("\033[32m%s\033[0m\n------------------------------------------------------\n"
" url: %s\n size: %"PRId64"\n info-hash: %s\n uuid: %s\n description: %s\n"
" comment: %s\n category: %s\n"
, i->title.c_str(), i->url.c_str(), i->size
, i->info_hash.is_all_zeros() ? "" : to_hex(i->info_hash.to_string()).c_str()
, i->uuid.c_str(), i->description.c_str(), i->comment.c_str(), i->category.c_str());
}
}
std::string const& progress_bar(int progress, int width)
{
static std::string bar;
bar.clear();
bar.reserve(width + 10);
int progress_chars = (progress * width + 500) / 1000;
std::fill_n(std::back_inserter(bar), progress_chars, '#');
std::fill_n(std::back_inserter(bar), width - progress_chars, '-');
return bar;
}
int save_file(std::string const& filename, std::vector<char>& v)
{
using namespace libtorrent;
file f;
error_code ec;
if (!f.open(filename, file::write_only, ec)) return -1;
if (ec) return -1;
file::iovec_t b = {&v[0], v.size()};
size_type written = f.writev(0, &b, 1, ec);
if (written != v.size()) return -3;
if (ec) return -3;
return 0;
}
volatile bool quit = false;
void sig(int num)
{
quit = true;
}
int main(int argc, char* argv[])
{
if ((argc == 2 && strcmp(argv[1], "--help") == 0) || argc > 2)
{
std::cerr << "usage: rss_reader [rss-url]\n";
return 0;
}
session ses;
session_settings sett;
sett.active_downloads = 2;
sett.active_seeds = 1;
sett.active_limit = 3;
ses.set_settings(sett);
std::vector<char> in;
if (load_file(".ses_state", in) == 0)
{
lazy_entry e;
error_code ec;
if (lazy_bdecode(&in[0], &in[0] + in.size(), e, ec) == 0)
ses.load_state(e);
}
feed_handle fh;
if (argc == 2)
{
feed_settings feed;
feed.url = argv[1];
feed.add_args.save_path = ".";
fh = ses.add_feed(feed);
fh.update_feed();
}
else
{
std::vector<feed_handle> handles;
ses.get_feeds(handles);
if (handles.empty())
{
printf("usage: rss_reader rss-url\n");
return 1;
}
fh = handles[0];
}
feed_status fs = fh.get_feed_status();
int i = 0;
char spinner[] = {'|', '/', '-', '\\'};
fprintf(stderr, "fetching feed ... %c", spinner[i]);
while (fs.updating)
{
sleep(100);
i = (i + 1) % 4;
fprintf(stderr, "\b%c", spinner[i]);
fs = fh.get_feed_status();
}
fprintf(stderr, "\bDONE\n");
print_feed(fs);
signal(SIGTERM, &sig);
signal(SIGINT, &sig);
while (!quit)
{
std::vector<torrent_handle> t = ses.get_torrents();
for (std::vector<torrent_handle>::iterator i = t.begin()
, end(t.end()); i != end; ++i)
{
torrent_status st = i->status();
std::string const& progress = progress_bar(st.progress_ppm / 1000, 40);
std::string name = i->name();
if (name.size() > 70) name.resize(70);
std::string error = st.error;
if (error.size() > 40) error.resize(40);
static char const* state_str[] =
{"checking (q)", "checking", "dl metadata"
, "downloading", "finished", "seeding", "allocating", "checking (r)"};
std::string status = st.paused ? "queued" : state_str[st.state];
int attribute = 0;
if (st.paused) attribute = 33;
else if (st.state == torrent_status::downloading) attribute = 1;
printf("\033[%dm%2d %-70s d:%-4d u:%-4d %-40s %4d(%4d) %-12s\033[0m\n"
, attribute, st.queue_position
, name.c_str(), st.download_rate / 1000
, st.upload_rate / 1000, !error.empty() ? error.c_str() : progress.c_str()
, st.num_peers, st.num_seeds, status.c_str());
}
sleep(500);
if (quit) break;
printf("\033[%dA", int(t.size()));
}
printf("saving session state\n");
{
entry session_state;
ses.save_state(session_state);
std::vector<char> out;
bencode(std::back_inserter(out), session_state);
save_file(".ses_state", out);
}
printf("closing session");
return 0;
}

View File

@ -78,6 +78,7 @@ nobase_include_HEADERS = \
proxy_base.hpp \ proxy_base.hpp \
ptime.hpp \ ptime.hpp \
puff.hpp \ puff.hpp \
rss.hpp \
session.hpp \ session.hpp \
session_settings.hpp \ session_settings.hpp \
session_status.hpp \ session_status.hpp \

View File

@ -86,6 +86,8 @@ namespace libtorrent
bool share_mode; bool share_mode;
std::string trackerid; std::string trackerid;
std::string url; std::string url;
std::string uuid;
std::string source_feed_url;
}; };
} }

View File

@ -88,6 +88,7 @@ namespace libtorrent {
performance_warning = 0x200, performance_warning = 0x200,
dht_notification = 0x400, dht_notification = 0x400,
stats_notification = 0x800, stats_notification = 0x800,
rss_notification = 0x1000,
all_categories = 0xffffffff all_categories = 0xffffffff
}; };

View File

@ -41,6 +41,7 @@ POSSIBILITY OF SUCH DAMAGE.
#include "libtorrent/identify_client.hpp" #include "libtorrent/identify_client.hpp"
#include "libtorrent/address.hpp" #include "libtorrent/address.hpp"
#include "libtorrent/stat.hpp" #include "libtorrent/stat.hpp"
#include "libtorrent/rss.hpp" // for feed_handle
// lines reserved for future includes // lines reserved for future includes
// the type-ids of the alert types // the type-ids of the alert types
@ -50,7 +51,6 @@ POSSIBILITY OF SUCH DAMAGE.
namespace libtorrent namespace libtorrent
{ {
struct TORRENT_EXPORT torrent_alert: alert struct TORRENT_EXPORT torrent_alert: alert
@ -1194,6 +1194,29 @@ namespace libtorrent
virtual std::string message() const; virtual std::string message() const;
}; };
struct TORRENT_EXPORT rss_alert: alert
{
rss_alert(feed_handle h, std::string const& url_, int state_, error_code const& ec)
: handle(h), url(url_), state(state_), error(ec)
{}
TORRENT_DEFINE_ALERT(rss_alert);
const static int static_category = alert::rss_notification;
virtual std::string message() const;
enum state_t
{
state_updating, state_updated, state_error
};
feed_handle handle;
std::string url;
int state;
error_code error;
};
} }

View File

@ -84,6 +84,7 @@ POSSIBILITY OF SUCH DAMAGE.
#include "libtorrent/address.hpp" #include "libtorrent/address.hpp"
#include "libtorrent/utp_socket_manager.hpp" #include "libtorrent/utp_socket_manager.hpp"
#include "libtorrent/bloom_filter.hpp" #include "libtorrent/bloom_filter.hpp"
#include "libtorrent/rss.hpp"
#ifdef TORRENT_STATS #ifdef TORRENT_STATS
#include <fstream> #include <fstream>
@ -201,7 +202,13 @@ namespace libtorrent
} }
#endif #endif
boost::weak_ptr<torrent> find_torrent(const sha1_hash& info_hash); feed_handle add_feed(feed_settings const& feed);
void remove_feed(feed_handle h);
void get_feeds(std::vector<feed_handle>* f) const;
boost::weak_ptr<torrent> find_torrent(sha1_hash const& info_hash);
boost::weak_ptr<torrent> find_torrent(std::string const& uuid);
peer_id const& get_peer_id() const { return m_peer_id; } peer_id const& get_peer_id() const { return m_peer_id; }
void close_connection(peer_connection const* p, error_code const& ec); void close_connection(peer_connection const* p, error_code const& ec);
@ -551,6 +558,8 @@ namespace libtorrent
tracker_manager m_tracker_manager; tracker_manager m_tracker_manager;
torrent_map m_torrents; torrent_map m_torrents;
std::map<std::string, boost::shared_ptr<torrent> > m_uuids;
typedef std::list<boost::shared_ptr<torrent> > check_queue_t; typedef std::list<boost::shared_ptr<torrent> > check_queue_t;
// this has all torrents that wants to be checked in it // this has all torrents that wants to be checked in it
@ -726,6 +735,13 @@ namespace libtorrent
// to decide which ones to choke/unchoke // to decide which ones to choke/unchoke
ptime m_last_choke; ptime m_last_choke;
// the time when the next rss feed needs updating
ptime m_next_rss_update;
// update any rss feeds that need updating and
// recalculate m_next_rss_update
void update_rss_feeds();
// when outgoing_ports is configured, this is the // when outgoing_ports is configured, this is the
// port we'll bind the next outgoing socket to // port we'll bind the next outgoing socket to
int m_next_port; int m_next_port;
@ -900,6 +916,8 @@ namespace libtorrent
size_type m_total_failed_bytes; size_type m_total_failed_bytes;
size_type m_total_redundant_bytes; size_type m_total_redundant_bytes;
std::vector<boost::shared_ptr<feed> > m_feeds;
// the main working thread // the main working thread
boost::scoped_ptr<thread> m_thread; boost::scoped_ptr<thread> m_thread;

174
include/libtorrent/rss.hpp Normal file
View File

@ -0,0 +1,174 @@
/*
Copyright (c) 2010, Arvid Norberg
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef TORRENT_RSS_HPP_INCLUDED
#define TORRENT_RSS_HPP_INCLUDED
#include "libtorrent/torrent_handle.hpp"
#include "libtorrent/add_torrent_params.hpp"
#include "libtorrent/size_type.hpp"
#include <boost/enable_shared_from_this.hpp>
#include <string>
namespace libtorrent
{
namespace aux
{ struct session_impl; }
struct feed_item
{
feed_item(): size(-1) {}
std::string url;
std::string uuid;
std::string title;
std::string description;
std::string comment;
std::string category;
size_type size;
torrent_handle handle;
sha1_hash info_hash;
};
#ifndef BOOST_NO_EXCEPTIONS
torrent_handle add_feed_item(session& s, feed_item const& fi
, add_torrent_params const& p);
#endif
torrent_handle add_feed_item(session& s, feed_item const& fi
, add_torrent_params const& p, error_code& ec);
// the feed_settings object is all the information
// and configuration for a specific feed. All of
// these settings can be changed by the user
// after adding the feed
struct feed_settings
{
feed_settings()
: auto_download(true)
, default_ttl(30)
{}
std::string url;
bool auto_download;
// in minutes
int default_ttl;
// used when adding torrents
add_torrent_params add_args;
};
struct feed_status
{
std::string url;
std::string title;
std::string description;
time_t last_update;
int next_update;
bool updating;
std::vector<feed_item> items;
error_code error;
int ttl;
};
struct feed;
struct feed_handle
{
feed_handle() {}
void update_feed();
feed_status get_feed_status() const;
void set_settings(feed_settings const& s);
feed_settings settings() const;
private:
friend struct aux::session_impl;
friend struct feed;
feed_handle(boost::weak_ptr<feed> const& p);
boost::weak_ptr<feed> m_feed_ptr;
};
struct feed_state;
struct http_parser;
boost::shared_ptr<feed> new_feed(aux::session_impl& ses, feed_settings const& sett);
// this is the internal object holding all state about an
// RSS feed. All user interaction with this object
// goes through the feed_handle, which makes sure all calls
// are posted to the network thread
struct feed : boost::enable_shared_from_this<feed>
{
friend void parse_feed(feed_state& f, int token, char const* name, char const* val);
feed(aux::session_impl& ses, feed_settings const& feed);
void on_feed(error_code const& ec, http_parser const& parser
, char const* data, int size);
void update_feed();
aux::session_impl& session() const { return m_ses; }
void set_settings(feed_settings const& s);
void get_settings(feed_settings* s) const;
void get_feed_status(feed_status* ret) const;
int next_update(time_t now) const;
void load_state(lazy_entry const& rd);
void save_state(entry& rd) const;
// private:
feed_handle my_handle();
error_code m_error;
std::vector<feed_item> m_items;
std::string m_title;
std::string m_description;
time_t m_last_attempt;
time_t m_last_update;
// refresh rate of this feed in minutes
int m_ttl;
// true while waiting for the server to respond
bool m_updating;
feed_settings m_settings;
aux::session_impl& m_ses;
};
};
#endif

View File

@ -56,6 +56,7 @@ POSSIBILITY OF SUCH DAMAGE.
#include "libtorrent/peer_id.hpp" #include "libtorrent/peer_id.hpp"
#include "libtorrent/alert.hpp" // alert::error_notification #include "libtorrent/alert.hpp" // alert::error_notification
#include "libtorrent/add_torrent_params.hpp" #include "libtorrent/add_torrent_params.hpp"
#include "libtorrent/rss.hpp"
#include "libtorrent/storage.hpp" #include "libtorrent/storage.hpp"
@ -160,7 +161,8 @@ namespace libtorrent
save_proxy = 0x008, save_proxy = 0x008,
save_i2p_proxy = 0x010, save_i2p_proxy = 0x010,
save_encryption_settings = 0x020, save_encryption_settings = 0x020,
save_as_map = 0x040 save_as_map = 0x040,
save_feeds = 0x080
#ifndef TORRENT_NO_DEPRECATE #ifndef TORRENT_NO_DEPRECATE
, ,
@ -237,6 +239,10 @@ namespace libtorrent
void get_cache_info(sha1_hash const& ih void get_cache_info(sha1_hash const& ih
, std::vector<cached_piece_info>& ret) const; , std::vector<cached_piece_info>& ret) const;
feed_handle add_feed(feed_settings const& feed);
void remove_feed(feed_handle h);
void get_feeds(std::vector<feed_handle>& f) const;
#ifndef TORRENT_DISABLE_DHT #ifndef TORRENT_DISABLE_DHT
void start_dht(); void start_dht();
void stop_dht(); void stop_dht();

View File

@ -41,8 +41,9 @@ namespace libtorrent
struct lazy_entry; struct lazy_entry;
struct entry; struct entry;
enum { std_string = 0, character = 1, short_integer = 2 enum { std_string, character, integer
, integer = 3, floating_point = 4, boolean = 5}; , floating_point, boolean, size_integer
, time_integer };
// this is used to map struct entries // this is used to map struct entries
// to names in a bencoded dictionary to // to names in a bencoded dictionary to

View File

@ -699,6 +699,7 @@ namespace libtorrent
torrent_info const& torrent_file() const torrent_info const& torrent_file() const
{ return *m_torrent_file; } { return *m_torrent_file; }
std::string const& uuid() const { return m_uuid; }
std::string const& url() const { return m_url; } std::string const& url() const { return m_url; }
std::vector<announce_entry> const& trackers() const std::vector<announce_entry> const& trackers() const
@ -979,6 +980,14 @@ namespace libtorrent
// the torrent file // the torrent file
std::string m_url; std::string m_url;
// if this was added from an RSS feed, this is the unique
// identifier in the feed.
std::string m_uuid;
// if this torrent was added by an RSS feed, this is the
// URL to that feed
std::string m_source_feed_url;
// this is used as temporary storage while downloading // this is used as temporary storage while downloading
// the .torrent file from m_url // the .torrent file from m_url
std::vector<char> m_torrent_file_buf; std::vector<char> m_torrent_file_buf;

View File

@ -420,6 +420,7 @@ namespace libtorrent
{ {
friend class invariant_access; friend class invariant_access;
friend struct aux::session_impl; friend struct aux::session_impl;
friend struct feed;
friend class torrent; friend class torrent;
torrent_handle() {} torrent_handle() {}

View File

@ -63,6 +63,7 @@ libtorrent_rasterbar_la_SOURCES = \
packet_buffer.cpp \ packet_buffer.cpp \
policy.cpp \ policy.cpp \
puff.cpp \ puff.cpp \
rss.cpp \
session.cpp \ session.cpp \
session_impl.cpp \ session_impl.cpp \
settings.cpp \ settings.cpp \

View File

@ -496,5 +496,14 @@ namespace libtorrent {
return "DHT bootstrap complete"; return "DHT bootstrap complete";
} }
std::string rss_alert::message() const
{
char msg[600];
char const* state_msg[] = {"updating", "updated", "error"};
snprintf(msg, sizeof(msg), "RSS feed %s: %s (%s)"
, url.c_str(), state_msg[state], error.message().c_str());
return msg;
}
} // namespace libtorrent } // namespace libtorrent

View File

@ -116,7 +116,7 @@ void http_connection::get(std::string const& url, time_duration timeout, int pri
{ {
// if we're using an http proxy and not an ssl // if we're using an http proxy and not an ssl
// connection, just do a regular http proxy request // connection, just do a regular http proxy request
APPEND_FMT1("GET %s HTTP/1.0\r\n", url.c_str()); APPEND_FMT1("GET %s HTTP/1.1\r\n", url.c_str());
if (ps->type == proxy_settings::http_pw) if (ps->type == proxy_settings::http_pw)
APPEND_FMT1("Proxy-Authorization: Basic %s\r\n", base64encode( APPEND_FMT1("Proxy-Authorization: Basic %s\r\n", base64encode(
ps->username + ":" + ps->password).c_str()); ps->username + ":" + ps->password).c_str());
@ -125,7 +125,7 @@ void http_connection::get(std::string const& url, time_duration timeout, int pri
} }
else else
{ {
APPEND_FMT2("GET %s HTTP/1.0\r\n" APPEND_FMT2("GET %s HTTP/1.1\r\n"
"Host: %s", path.c_str(), hostname.c_str()); "Host: %s", path.c_str(), hostname.c_str());
if (port != default_port) APPEND_FMT1(":%d\r\n", port); if (port != default_port) APPEND_FMT1(":%d\r\n", port);
else APPEND_FMT("\r\n"); else APPEND_FMT("\r\n");

580
src/rss.cpp Normal file
View File

@ -0,0 +1,580 @@
/*
Copyright (c) 2010, Arvid Norberg
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include "libtorrent/rss.hpp"
#include "libtorrent/xml_parse.hpp"
#include "libtorrent/http_parser.hpp"
#include "libtorrent/http_connection.hpp"
#include "libtorrent/aux_/session_impl.hpp"
#include "libtorrent/session.hpp"
#include "libtorrent/settings.hpp"
#include "libtorrent/alert_types.hpp" // for rss_alert
#include <boost/bind.hpp>
namespace libtorrent {
struct feed_state
{
feed_state(feed& r)
: in_item(false)
, type(none)
, ret(r)
{}
bool in_item;
std::string current_tag;
enum feed_type
{
none, atom, rss2
} type;
feed_item current_item;
feed& ret;
bool is_item(char const* tag) const
{
switch (type)
{
case atom: return string_equal_no_case(tag, "entry");
case rss2: return string_equal_no_case(tag, "item");
default: return false;
}
}
bool is_title(char const* tag) const
{
switch (type)
{
case atom:
case rss2: return string_equal_no_case(tag, "title");
default: return false;
}
}
bool is_url(char const* tag) const
{
switch (type)
{
case atom:
case rss2: return string_equal_no_case(tag, "link");
default: return false;
}
}
bool is_desc(char const* tag) const
{
switch (type)
{
case atom: return string_equal_no_case(tag, "summary");
case rss2: return string_equal_no_case(tag, "description")
|| string_equal_no_case(tag, "media:text");
default: return false;
}
}
bool is_uuid(char const* tag) const
{
switch (type)
{
case atom: return string_equal_no_case(tag, "id");
case rss2: return string_equal_no_case(tag, "guid");
default: return false;
}
}
bool is_comment(char const* tag) const
{
switch (type)
{
case atom: return false;
case rss2: return string_equal_no_case(tag, "comments");
default: return false;
}
}
bool is_category(char const* tag) const
{
switch (type)
{
case atom: return false;
case rss2: return string_equal_no_case(tag, "category");
default: return false;
}
}
bool is_size(char const* tag) const
{
return string_equal_no_case(tag, "size");
}
bool is_hash(char const* tag) const
{
return string_equal_no_case(tag, "hash")
|| string_equal_no_case(tag, "media:hash");
}
bool is_ttl(char const* tag) const
{
return string_equal_no_case(tag, "ttl");
}
};
void parse_feed(feed_state& f, int token, char const* name, char const* val)
{
switch (token)
{
case xml_parse_error:
f.ret.m_error = errors::parse_failed;
return;
case xml_start_tag:
case xml_empty_tag:
{
f.current_tag = name;
if (f.type == feed_state::none)
{
if (string_equal_no_case(f.current_tag.c_str(), "feed"))
f.type = feed_state::atom;
else if (string_equal_no_case(f.current_tag.c_str(), "rss"))
f.type = feed_state::rss2;
}
if (f.is_item(name)) f.in_item = true;
return;
}
case xml_attribute:
{
if (!f.in_item) return;
if (f.is_url(f.current_tag.c_str())
&& f.type == feed_state::atom)
{
// atom feeds have items like this:
// <link href="http://..." length="12345"/>
if (string_equal_no_case(name, "href"))
f.current_item.url = val;
else if (string_equal_no_case(name, "length"))
f.current_item.size = strtoll(val, 0, 10);
}
else if (f.type == feed_state::rss2
&& string_equal_no_case(f.current_tag.c_str(), "enclosure"))
{
// rss feeds have items like this:
// <enclosure url="http://..." length="12345"/>
if (string_equal_no_case(name, "url"))
f.current_item.url = val;
else if (string_equal_no_case(name, "length"))
f.current_item.size = strtoll(val, 0, 10);
}
else if (f.type == feed_state::rss2
&& string_equal_no_case(f.current_tag.c_str(), "media:content"))
{
// rss feeds sometimes have items like this:
// <media:content url="http://..." filesize="12345"/>
if (string_equal_no_case(name, "url"))
f.current_item.url = val;
else if (string_equal_no_case(name, "filesize"))
f.current_item.size = strtoll(val, 0, 10);
}
return;
}
case xml_end_tag:
{
if (f.in_item && f.is_item(name))
{
f.in_item = false;
if (!f.current_item.title.empty()
&& !f.current_item.url.empty())
f.ret.m_items.push_back(f.current_item);
f.current_item = feed_item();
}
f.current_tag = "";
return;
}
case xml_string:
{
if (!f.in_item)
{
if (f.is_title(f.current_tag.c_str()))
f.ret.m_title = name;
else if (f.is_desc(f.current_tag.c_str()))
f.ret.m_description = name;
else if (f.is_ttl(f.current_tag.c_str()))
{
int tmp = atoi(name);
if (tmp > 0) f.ret.m_ttl = tmp;
}
return;
}
if (f.is_title(f.current_tag.c_str()))
f.current_item.title = name;
else if (f.is_desc(f.current_tag.c_str()))
f.current_item.description = name;
else if (f.is_uuid(f.current_tag.c_str()))
f.current_item.uuid = name;
else if (f.is_url(f.current_tag.c_str()) && f.type != feed_state::atom)
f.current_item.url = name;
else if (f.is_comment(f.current_tag.c_str()))
f.current_item.comment = name;
else if (f.is_category(f.current_tag.c_str()))
f.current_item.category = name;
else if (f.is_size(f.current_tag.c_str()))
f.current_item.size = strtoll(name, 0, 10);
else if (f.is_hash(f.current_tag.c_str()) && strlen(name) == 40)
{
if (!from_hex(name, 40, (char*)&f.current_item.info_hash[0]))
{
// hex parsing failed
f.current_item.info_hash.clear();
}
}
return;
}
case xml_declaration_tag: return;
case xml_comment: return;
}
}
torrent_handle add_feed_item(session& s, feed_item const& fi
, add_torrent_params const& tp, error_code& ec)
{
add_torrent_params p = tp;
p.url = fi.url;
p.uuid = fi.uuid;
// #error figure out how to get the feed url in here
// p.source_feed_url = ???;
p.ti.reset();
p.info_hash.clear();
p.name = fi.title.c_str();
return s.add_torrent(p, ec);
}
#ifndef BOOST_NO_EXCEPTIONS
torrent_handle add_feed_item(session& s, feed_item const& fi
, add_torrent_params const& tp)
{
error_code ec;
torrent_handle ret = add_feed_item(s, fi, tp, ec);
if (ec) throw libtorrent_exception(ec);
return ret;
}
#endif
boost::shared_ptr<feed> new_feed(aux::session_impl& ses, feed_settings const& sett)
{
return boost::shared_ptr<feed>(new feed(ses, sett));
}
feed::feed(aux::session_impl& ses, feed_settings const& sett)
: m_last_attempt(0)
, m_last_update(0)
, m_ttl(-1)
, m_updating(false)
, m_settings(sett)
, m_ses(ses)
{
}
void feed::set_settings(feed_settings const& s)
{
m_settings = s;
}
void feed::get_settings(feed_settings* s) const
{
*s = m_settings;
}
feed_handle feed::my_handle()
{
return feed_handle(boost::weak_ptr<feed>(shared_from_this()));
}
void feed::on_feed(error_code const& ec
, http_parser const& parser, char const* data, int size)
{
TORRENT_ASSERT(m_updating);
m_updating = false;
if (ec && ec != asio::error::eof)
{
m_error = ec;
if (m_ses.m_alerts.should_post<rss_alert>())
{
m_ses.m_alerts.post_alert(rss_alert(my_handle(), m_settings.url
, rss_alert::state_error, m_error));
}
return;
}
if (parser.status_code() != 200)
{
m_error = error_code(parser.status_code(), get_http_category());
if (m_ses.m_alerts.should_post<rss_alert>())
{
m_ses.m_alerts.post_alert(rss_alert(my_handle(), m_settings.url
, rss_alert::state_error, m_error));
}
return;
}
char* buf = const_cast<char*>(data);
feed_state s(*this);
xml_parse(buf, buf + size, boost::bind(&parse_feed, boost::ref(s), _1, _2, _3));
for (std::vector<feed_item>::iterator i = m_items.begin()
, end(m_items.end()); i != end; ++i)
{
i->handle = torrent_handle(m_ses.find_torrent(i->uuid.empty() ? i->url : i->uuid));
// if we're already downloading this torrent, or if we
// don't have auto-download enabled, just move along to
// the next one
if (i->handle.is_valid() || !m_settings.auto_download) continue;
// this means we should add this torrent to the session
add_torrent_params p = m_settings.add_args;
p.url = i->url;
p.uuid = i->uuid;
p.source_feed_url = m_settings.url;
p.ti.reset();
p.info_hash.clear();
p.name = i->title.c_str();
error_code ec;
// #error session_impl::add_torrent doesn't support magnet links via url
m_ses.add_torrent(p, ec);
if (ec)
{
// #error alert!
}
}
m_last_update = time(0);
// report that we successfully updated the feed
if (m_ses.m_alerts.should_post<rss_alert>())
{
m_ses.m_alerts.post_alert(rss_alert(my_handle(), m_settings.url
, rss_alert::state_updated, error_code()));
}
// update m_ses.m_next_rss_update timestamps
// now that we have updated our timestamp
m_ses.update_rss_feeds();
}
#define TORRENT_SETTING(t, x) {#x, offsetof(feed_settings,x), t},
bencode_map_entry feed_settings_map[] =
{
TORRENT_SETTING(std_string, url)
TORRENT_SETTING(boolean, auto_download)
TORRENT_SETTING(integer, default_ttl)
};
#undef TORRENT_SETTING
#define TORRENT_SETTING(t, x) {#x, offsetof(feed_item,x), t},
bencode_map_entry feed_item_map[] =
{
TORRENT_SETTING(std_string, url)
TORRENT_SETTING(std_string, uuid)
TORRENT_SETTING(std_string, title)
TORRENT_SETTING(std_string, description)
TORRENT_SETTING(std_string, comment)
TORRENT_SETTING(std_string, category)
TORRENT_SETTING(size_integer, size)
};
#undef TORRENT_SETTING
#define TORRENT_SETTING(t, x) {#x, offsetof(feed,x), t},
bencode_map_entry feed_map[] =
{
TORRENT_SETTING(std_string, m_title)
TORRENT_SETTING(std_string, m_description)
TORRENT_SETTING(time_integer, m_last_attempt)
TORRENT_SETTING(time_integer, m_last_update)
};
#undef TORRENT_SETTING
#define TORRENT_SETTING(t, x) {#x, offsetof(add_torrent_params,x), t},
bencode_map_entry add_torrent_map[] =
{
TORRENT_SETTING(std_string, save_path)
TORRENT_SETTING(boolean, paused)
TORRENT_SETTING(boolean, auto_managed)
TORRENT_SETTING(boolean, duplicate_is_error)
TORRENT_SETTING(boolean, seed_mode)
TORRENT_SETTING(boolean, override_resume_data)
TORRENT_SETTING(boolean, upload_mode)
TORRENT_SETTING(boolean, share_mode)
TORRENT_SETTING(std_string, trackerid)
};
#undef TORRENT_SETTING
void feed::load_state(lazy_entry const& rd)
{
load_struct(rd, this, feed_map, sizeof(feed_map)/sizeof(feed_map[0]));
lazy_entry const* e = rd.dict_find_list("items");
if (e)
{
m_items.reserve(e->list_size());
for (int i = 0; i < e->list_size(); ++i)
{
if (e->list_at(i)->type() != lazy_entry::dict_t) continue;
m_items.push_back(feed_item());
load_struct(*e->list_at(i), &m_items.back(), feed_item_map
, sizeof(feed_item_map)/sizeof(feed_item_map[0]));
}
}
load_struct(rd, &m_settings, feed_settings_map
, sizeof(feed_settings_map)/sizeof(feed_settings_map[0]));
e = rd.dict_find_dict("add_params");
if (e)
{
load_struct(*e, &m_settings.add_args, add_torrent_map
, sizeof(add_torrent_map)/sizeof(add_torrent_map[0]));
}
}
void feed::save_state(entry& rd) const
{
save_struct(rd, this, feed_map, sizeof(feed_map)/sizeof(feed_map[0]));
entry::list_type& items = rd["items"].list();
for (std::vector<feed_item>::const_iterator i = m_items.begin()
, end(m_items.end()); i != end; ++i)
{
items.push_back(entry());
entry& item = items.back();
save_struct(item, &*i, feed_item_map, sizeof(feed_item_map)/sizeof(feed_item_map[0]));
}
feed_settings sett_def;
save_struct(rd, &m_settings, feed_settings_map
, sizeof(feed_settings_map)/sizeof(feed_settings_map[0]), &sett_def);
entry& add = rd["add_params"];
add_torrent_params add_def;
save_struct(add, &m_settings.add_args, add_torrent_map
, sizeof(add_torrent_map)/sizeof(add_torrent_map[0]), &add_def);
}
void feed::update_feed()
{
if (m_updating) return;
m_last_attempt = time(0);
if (m_ses.m_alerts.should_post<rss_alert>())
{
m_ses.m_alerts.post_alert(rss_alert(my_handle(), m_settings.url
, rss_alert::state_updating, error_code()));
}
boost::shared_ptr<http_connection> feed(
new http_connection(m_ses.m_io_service, m_ses.m_half_open
, boost::bind(&feed::on_feed, shared_from_this()
, _1, _2, _3, _4)));
m_updating = true;
feed->get(m_settings.url, seconds(30), 0, 0, 5, m_ses.m_settings.user_agent);
}
void feed::get_feed_status(feed_status* ret) const
{
ret->items = m_items;
ret->last_update = m_last_update;
ret->updating = m_updating;
ret->url = m_settings.url;
ret->title = m_title;
ret->description = m_description;
ret->error = m_error;
ret->ttl = m_ttl == -1 ? m_settings.default_ttl : m_ttl;
ret->next_update = next_update(time(0));
}
int feed::next_update(time_t now) const
{
int ttl = m_ttl == -1 ? m_settings.default_ttl : m_ttl;
return (m_last_update + ttl * 60) - now;
}
// defined in session.cpp
void fun_wrap(bool* done, condition* e, mutex* m, boost::function<void(void)> f);
#define TORRENT_ASYNC_CALL(x) \
boost::shared_ptr<feed> f = m_feed_ptr.lock(); \
if (!f) return; \
aux::session_impl& ses = f->session(); \
ses.m_io_service.post(boost::bind(&feed:: x, f))
#define TORRENT_ASYNC_CALL1(x, a1) \
boost::shared_ptr<feed> f = m_feed_ptr.lock(); \
if (!f) return; \
aux::session_impl& ses = f->session(); \
ses.m_io_service.post(boost::bind(&feed:: x, f, a1))
#define TORRENT_SYNC_CALL1(x, a1) \
boost::shared_ptr<feed> f = m_feed_ptr.lock(); \
if (f) { \
bool done = false; \
aux::session_impl& ses = f->session(); \
mutex::scoped_lock l(ses.mut); \
ses.m_io_service.post(boost::bind(&fun_wrap, &done, &ses.cond, &ses.mut, boost::function<void(void)>(boost::bind(&feed:: x, f, a1)))); \
f.reset(); \
do { ses.cond.wait(l); } while(!done); }
feed_handle::feed_handle(boost::weak_ptr<feed> const& p)
: m_feed_ptr(p) {}
void feed_handle::update_feed()
{
TORRENT_ASYNC_CALL(update_feed);
}
feed_status feed_handle::get_feed_status() const
{
feed_status ret;
TORRENT_SYNC_CALL1(get_feed_status, &ret);
return ret;
}
void feed_handle::set_settings(feed_settings const& s)
{
TORRENT_SYNC_CALL1(set_settings, s);
}
feed_settings feed_handle::settings() const
{
feed_settings ret;
TORRENT_SYNC_CALL1(get_settings, &ret);
return ret;
}
};

View File

@ -375,6 +375,25 @@ namespace libtorrent
TORRENT_SYNC_CALL1(load_state, &e); TORRENT_SYNC_CALL1(load_state, &e);
} }
feed_handle session::add_feed(feed_settings const& feed)
{
// if you have auto-download enabled, you must specify a download directory!
TORRENT_ASSERT(!feed.auto_download || !feed.add_args.save_path.empty());
TORRENT_SYNC_CALL_RET1(feed_handle, add_feed, feed);
return r;
}
void session::remove_feed(feed_handle h)
{
TORRENT_ASYNC_CALL1(remove_feed, h);
}
void session::get_feeds(std::vector<feed_handle>& f) const
{
f.clear();
TORRENT_SYNC_CALL1(get_feeds, &f);
}
#ifndef TORRENT_DISABLE_EXTENSIONS #ifndef TORRENT_DISABLE_EXTENSIONS
void session::add_extension(boost::function<boost::shared_ptr<torrent_plugin>(torrent*, void*)> ext) void session::add_extension(boost::function<boost::shared_ptr<torrent_plugin>(torrent*, void*)> ext)
{ {

View File

@ -500,6 +500,7 @@ namespace aux {
, m_last_tick(m_created) , m_last_tick(m_created)
, m_last_second_tick(m_created - milliseconds(900)) , m_last_second_tick(m_created - milliseconds(900))
, m_last_choke(m_created) , m_last_choke(m_created)
, m_next_rss_update(min_time())
#ifndef TORRENT_DISABLE_DHT #ifndef TORRENT_DISABLE_DHT
, m_dht_announce_timer(m_io_service) , m_dht_announce_timer(m_io_service)
#endif #endif
@ -914,7 +915,6 @@ namespace aux {
{ {
e["dht state"] = m_dht->state(); e["dht state"] = m_dht->state();
} }
#endif #endif
#if TORRENT_USE_I2P #if TORRENT_USE_I2P
@ -940,6 +940,17 @@ namespace aux {
} }
#endif #endif
if (flags & session::save_feeds)
{
entry::list_type& feeds = e["feeds"].list();
for (std::vector<boost::shared_ptr<feed> >::const_iterator i =
m_feeds.begin(), end(m_feeds.end()); i != end; ++i)
{
feeds.push_back(entry());
(*i)->save_state(feeds.back());
}
}
} }
void session_impl::set_proxy(proxy_settings const& s) void session_impl::set_proxy(proxy_settings const& s)
@ -1018,6 +1029,19 @@ namespace aux {
m_lsd->use_broadcast(true); m_lsd->use_broadcast(true);
update_disk_thread_settings(); update_disk_thread_settings();
settings = e->dict_find_list("feeds");
if (settings)
{
m_feeds.reserve(settings->list_size());
for (int i = 0; i < settings->list_size(); ++i)
{
boost::shared_ptr<feed> f(new_feed(*this, feed_settings()));
if (settings->list_at(i)->type() != lazy_entry::dict_t) continue;
f->load_state(*settings->list_at(i));
m_feeds.push_back(f);
}
}
} }
#ifndef TORRENT_DISABLE_GEO_IP #ifndef TORRENT_DISABLE_GEO_IP
@ -1151,6 +1175,51 @@ namespace aux {
} }
#endif #endif
feed_handle session_impl::add_feed(feed_settings const& sett)
{
TORRENT_ASSERT(is_network_thread());
// look for duplicates. If we already have a feed with this
// URL, return a handle to the existing one
for (std::vector<boost::shared_ptr<feed> >::const_iterator i
= m_feeds.begin(), end(m_feeds.end()); i != end; ++i)
{
if (sett.url != (*i)->m_settings.url) continue;
return feed_handle(*i);
}
boost::shared_ptr<feed> f(new_feed(*this, sett));
m_feeds.push_back(f);
update_rss_feeds();
return feed_handle(f);
}
void session_impl::remove_feed(feed_handle h)
{
TORRENT_ASSERT(is_network_thread());
boost::shared_ptr<feed> f = h.m_feed_ptr.lock();
if (!f) return;
std::vector<boost::shared_ptr<feed> >::iterator i
= std::find(m_feeds.begin(), m_feeds.end(), f);
if (i == m_feeds.end()) return;
m_feeds.erase(i);
}
void session_impl::get_feeds(std::vector<feed_handle>* ret) const
{
TORRENT_ASSERT(is_network_thread());
ret->clear();
ret->reserve(m_feeds.size());
for (std::vector<boost::shared_ptr<feed> >::const_iterator i = m_feeds.begin()
, end(m_feeds.end()); i != end; ++i)
ret->push_back(feed_handle(*i));
}
void session_impl::pause() void session_impl::pause()
{ {
TORRENT_ASSERT(is_network_thread()); TORRENT_ASSERT(is_network_thread());
@ -2309,6 +2378,12 @@ namespace aux {
} }
} }
// --------------------------------------------------------------
// RSS feeds
// --------------------------------------------------------------
if (now > m_next_rss_update)
update_rss_feeds();
switch (m_settings.mixed_mode_algorithm) switch (m_settings.mixed_mode_algorithm)
{ {
case session_settings::prefer_tcp: case session_settings::prefer_tcp:
@ -2790,6 +2865,27 @@ namespace aux {
// m_peer_pool.release_memory(); // m_peer_pool.release_memory();
} }
void session_impl::update_rss_feeds()
{
time_t now_posix = time(0);
ptime min_update = max_time();
ptime now = time_now();
for (std::vector<boost::shared_ptr<feed> >::iterator i
= m_feeds.begin(), end(m_feeds.end()); i != end; ++i)
{
feed& f = **i;
int delta = f.next_update(now_posix);
if (delta <= 0)
{
f.update_feed();
continue;
}
ptime next_update = now + seconds(delta);
if (next_update < min_update) min_update = next_update;
}
m_next_rss_update = min_update;
}
#ifndef TORRENT_DISABLE_DHT #ifndef TORRENT_DISABLE_DHT
void session_impl::on_dht_announce(error_code const& e) void session_impl::on_dht_announce(error_code const& e)
@ -3404,6 +3500,16 @@ namespace aux {
return boost::weak_ptr<torrent>(); return boost::weak_ptr<torrent>();
} }
boost::weak_ptr<torrent> session_impl::find_torrent(std::string const& uuid)
{
TORRENT_ASSERT(is_network_thread());
std::map<std::string, boost::shared_ptr<torrent> >::iterator i
= m_uuids.find(uuid);
if (i != m_uuids.end()) return i->second;
return boost::weak_ptr<torrent>();
}
#if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING #if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING
boost::shared_ptr<logger> session_impl::create_log(std::string const& name boost::shared_ptr<logger> session_impl::create_log(std::string const& name
, int instance, bool append) , int instance, bool append)
@ -3469,6 +3575,8 @@ namespace aux {
// is the torrent already active? // is the torrent already active?
boost::shared_ptr<torrent> torrent_ptr = find_torrent(*ih).lock(); boost::shared_ptr<torrent> torrent_ptr = find_torrent(*ih).lock();
if (!torrent_ptr && !params.uuid.empty()) torrent_ptr = find_torrent(params.uuid).lock();
if (torrent_ptr) if (torrent_ptr)
{ {
if (!params.duplicate_is_error) if (!params.duplicate_is_error)
@ -3511,6 +3619,9 @@ namespace aux {
#endif #endif
m_torrents.insert(std::make_pair(*ih, torrent_ptr)); m_torrents.insert(std::make_pair(*ih, torrent_ptr));
if (!params.uuid.empty() || !params.url.empty())
m_uuids.insert(std::make_pair(params.uuid.empty()
? params.url : params.uuid, torrent_ptr));
// if this is an auto managed torrent, force a recalculation // if this is an auto managed torrent, force a recalculation
// of which torrents to have active // of which torrents to have active
@ -3547,9 +3658,10 @@ namespace aux {
{ {
TORRENT_ASSERT(*i == t || (*i)->should_check_files()); TORRENT_ASSERT(*i == t || (*i)->should_check_files());
if (*i == t) done = i; if (*i == t) done = i;
if (next_check == t || next_check->queue_position() > (*i)->queue_position()) else if (next_check == t || next_check->queue_position() > (*i)->queue_position())
next_check = *i; next_check = *i;
} }
TORRENT_ASSERT(next_check != t || m_queued_for_checking.size() == 1);
// only start a new one if we removed the one that is checking // only start a new one if we removed the one that is checking
TORRENT_ASSERT(done != m_queued_for_checking.end()); TORRENT_ASSERT(done != m_queued_for_checking.end());
if (done == m_queued_for_checking.end()) return; if (done == m_queued_for_checking.end()) return;
@ -3572,6 +3684,14 @@ namespace aux {
INVARIANT_CHECK; INVARIANT_CHECK;
// remove from uuid list
if (!tptr->uuid().empty())
{
std::map<std::string, boost::shared_ptr<torrent> >::iterator j
= m_uuids.find(tptr->uuid());
if (j != m_uuids.end()) m_uuids.erase(j);
}
session_impl::torrent_map::iterator i = session_impl::torrent_map::iterator i =
m_torrents.find(tptr->torrent_file().info_hash()); m_torrents.find(tptr->torrent_file().info_hash());

View File

@ -58,6 +58,8 @@ namespace libtorrent
case character: case character:
case boolean: case boolean:
case integer: case integer:
case size_integer:
case time_integer:
case floating_point: case floating_point:
{ {
if (key->type() != lazy_entry::int_t) continue; if (key->type() != lazy_entry::int_t) continue;
@ -66,6 +68,8 @@ namespace libtorrent
{ {
case character: *((char*)dest) = val; break; case character: *((char*)dest) = val; break;
case integer: *((int*)dest) = val; break; case integer: *((int*)dest) = val; break;
case size_integer: *((size_type*)dest) = val; break;
case time_integer: *((time_t*)dest) = val; break;
case floating_point: *((float*)dest) = float(val) / 1000.f; break; case floating_point: *((float*)dest) = float(val) / 1000.f; break;
case boolean: *((bool*)dest) = val; break; case boolean: *((bool*)dest) = val; break;
} }
@ -76,7 +80,7 @@ namespace libtorrent
void save_struct(entry& e, void const* s, bencode_map_entry const* m, int num, void const* def) void save_struct(entry& e, void const* s, bencode_map_entry const* m, int num, void const* def)
{ {
e = entry(entry::dictionary_t); if (e.type() != entry::dictionary_t) e = entry(entry::dictionary_t);
for (int i = 0; i < num; ++i) for (int i = 0; i < num; ++i)
{ {
char const* key = m[i].name; char const* key = m[i].name;
@ -97,6 +101,12 @@ namespace libtorrent
case integer: case integer:
if (*((int*)src) == *((int*)default_value)) continue; if (*((int*)src) == *((int*)default_value)) continue;
break; break;
case size_integer:
if (*((size_type*)src) == *((size_type*)default_value)) continue;
break;
case time_integer:
if (*((time_t*)src) == *((time_t*)default_value)) continue;
break;
case floating_point: case floating_point:
if (*((float*)src) == *((float*)default_value)) continue; if (*((float*)src) == *((float*)default_value)) continue;
break; break;
@ -113,6 +123,8 @@ namespace libtorrent
case std_string: val = *((std::string*)src); break; case std_string: val = *((std::string*)src); break;
case character: val = *((char*)src); break; case character: val = *((char*)src); break;
case integer: val = *((int*)src); break; case integer: val = *((int*)src); break;
case size_integer: val = *((size_type*)src); break;
case time_integer: val = *((time_t*)src); break;
case floating_point: val = size_type(*((float*)src) * 1000.f); break; case floating_point: val = size_type(*((float*)src) * 1000.f); break;
case boolean: val = *((bool*)src); break; case boolean: val = *((bool*)src); break;
default: TORRENT_ASSERT(false); default: TORRENT_ASSERT(false);

View File

@ -337,6 +337,8 @@ namespace libtorrent
, m_trackerid(p.trackerid) , m_trackerid(p.trackerid)
, m_save_path(complete(p.save_path)) , m_save_path(complete(p.save_path))
, m_url(p.url) , m_url(p.url)
, m_uuid(p.uuid)
, m_source_feed_url(p.source_feed_url)
, m_storage_constructor(p.storage) , m_storage_constructor(p.storage)
, m_ratio(0.f) , m_ratio(0.f)
, m_available_free_upload(0) , m_available_free_upload(0)
@ -400,6 +402,8 @@ namespace libtorrent
, m_graceful_pause_mode(false) , m_graceful_pause_mode(false)
, m_need_connect_boost(true) , m_need_connect_boost(true)
{ {
if (!m_url.empty() && m_uuid.empty()) m_uuid = m_url;
TORRENT_ASSERT(m_ses.is_network_thread()); TORRENT_ASSERT(m_ses.is_network_thread());
#if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING #if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING
(*m_ses.m_logger) << time_now_string() << " creating torrent: " (*m_ses.m_logger) << time_now_string() << " creating torrent: "
@ -442,6 +446,10 @@ namespace libtorrent
} }
} }
#if 1
// NON BOTTLED VERSION. SUPPORTS PROGRESS REPORTING
// since this download is not bottled, this callback will // since this download is not bottled, this callback will
// be called every time we receive another piece of the // be called every time we receive another piece of the
// .torrent file // .torrent file
@ -516,6 +524,85 @@ namespace libtorrent
TORRENT_ASSERT(num_torrents == m_ses.m_torrents.size()); TORRENT_ASSERT(num_torrents == m_ses.m_torrents.size());
// if the user added any trackers while downloading the
// .torrent file, serge them into the new tracker list
std::vector<announce_entry> new_trackers = m_torrent_file->trackers();
for (std::vector<announce_entry>::iterator i = m_trackers.begin()
, end(m_trackers.end()); i != end; ++i)
{
// if we already have this tracker, ignore it
if (std::find_if(new_trackers.begin(), new_trackers.end()
, boost::bind(&announce_entry::url, _1) == i->url) != new_trackers.end())
continue;
// insert the tracker ordered by tier
new_trackers.insert(std::find_if(new_trackers.begin(), new_trackers.end()
, boost::bind(&announce_entry::tier, _1) >= i->tier), *i);
}
m_trackers.swap(new_trackers);
#ifndef TORRENT_DISABLE_ENCRYPTION
hasher h;
h.update("req2", 4);
h.update((char*)&m_torrent_file->info_hash()[0], 20);
m_obfuscated_hash = h.final();
#endif
if (m_ses.m_alerts.should_post<metadata_received_alert>())
{
m_ses.m_alerts.post_alert(metadata_received_alert(
get_handle()));
}
set_state(torrent_status::downloading);
m_override_resume_data = true;
init();
announce_with_tracker();
}
#else
void torrent::on_torrent_download(error_code const& ec
, http_parser const& parser, char const* data, int size)
{
if (m_abort) return;
if (ec && ec != asio::error::eof)
{
set_error(ec, m_url);
pause();
return;
}
if (parser.status_code() != 200)
{
// #error there should really be an error code category for HTTP
set_error(errors::http_error, parser.message());
pause();
return;
}
error_code e;
intrusive_ptr<torrent_info> tf(new torrent_info(data, size, e));
if (e)
{
set_error(e, m_url);
pause();
return;
}
// update our torrent_info object and move the
// torrent from the old info-hash to the new one
// as we replace the torrent_info object
#ifdef TORRENT_DEBUG
int num_torrents = m_ses.m_torrents.size();
#endif
m_ses.m_torrents.erase(m_torrent_file->info_hash());
m_torrent_file = tf;
m_ses.m_torrents.insert(std::make_pair(m_torrent_file->info_hash(), shared_from_this()));
TORRENT_ASSERT(num_torrents == m_ses.m_torrents.size());
// TODO: if the user added any trackers while downloading the // TODO: if the user added any trackers while downloading the
// .torrent file, they are overwritten. Merge them into the // .torrent file, they are overwritten. Merge them into the
// new tracker list // new tracker list
@ -541,6 +628,8 @@ namespace libtorrent
announce_with_tracker(); announce_with_tracker();
} }
#endif
void torrent::start() void torrent::start()
{ {
TORRENT_ASSERT(m_ses.is_network_thread()); TORRENT_ASSERT(m_ses.is_network_thread());
@ -4086,6 +4175,9 @@ namespace libtorrent
m_last_download = rd.dict_find_int_value("last_download", 0); m_last_download = rd.dict_find_int_value("last_download", 0);
m_last_upload = rd.dict_find_int_value("last_upload", 0); m_last_upload = rd.dict_find_int_value("last_upload", 0);
m_url = rd.dict_find_string_value("url");
m_uuid = rd.dict_find_string_value("uuid");
m_added_time = rd.dict_find_int_value("added_time", m_added_time); m_added_time = rd.dict_find_int_value("added_time", m_added_time);
m_completed_time = rd.dict_find_int_value("completed_time", m_completed_time); m_completed_time = rd.dict_find_int_value("completed_time", m_completed_time);
if (m_completed_time != 0 && m_completed_time < m_added_time) if (m_completed_time != 0 && m_completed_time < m_added_time)
@ -4256,6 +4348,9 @@ namespace libtorrent
ret["last_download"] = m_last_download; ret["last_download"] = m_last_download;
ret["last_upload"] = m_last_upload; ret["last_upload"] = m_last_upload;
if (!m_url.empty()) ret["url"] = m_url;
if (!m_uuid.empty()) ret["uuid"] = m_uuid;
const sha1_hash& info_hash = torrent_file().info_hash(); const sha1_hash& info_hash = torrent_file().info_hash();
ret["info-hash"] = std::string((char*)info_hash.begin(), (char*)info_hash.end()); ret["info-hash"] = std::string((char*)info_hash.begin(), (char*)info_hash.end());

View File

@ -8,11 +8,12 @@
using namespace libtorrent; using namespace libtorrent;
void callback(int mapping, int port, error_code const& err) void callback(int mapping, address extip, int port, error_code const& err)
{ {
std::cerr std::cerr
<< "mapping: " << mapping << "mapping: " << mapping
<< ", port: " << port << ", port: " << port
<< ", external-IP: " << print_address(extip)
<< ", error: \"" << err.message() << "\"\n"; << ", error: \"" << err.message() << "\"\n";
} }