diff --git a/btpd/Makefile.am b/btpd/Makefile.am
index cb72911..a77def4 100644
--- a/btpd/Makefile.am
+++ b/btpd/Makefile.am
@@ -10,8 +10,7 @@ btpd_SOURCES=\
 	net_buf.c net_buf.h\
 	opts.c opts.h\
 	peer.c peer.h\
-	queue.h\
-	torrent.c torrent.h\
+	tlib.c tlib.h torrent.c torrent.h\
 	tracker_req.c tracker_req.h\
 	upload.c upload.h\
 	util.c
diff --git a/btpd/active.c b/btpd/active.c
index 8a2b0aa..9db9a04 100644
--- a/btpd/active.c
+++ b/btpd/active.c
@@ -83,8 +83,9 @@ active_start(void)
 
     pos = 0;
     while (fread(hash, sizeof(hash), 1, fp) == 1) {
-        if (torrent_get(hash) == NULL)
-            if (torrent_start(hash) != 0) {
+        struct tlib *tl = tlib_by_hash(hash);
+        if (tl != NULL && tl->tp == NULL)
+            if (torrent_start(tl) != 0) {
                 active_del_pos(fp, pos, &sb.st_size);
                 fseek(fp, pos, SEEK_SET);
             }
diff --git a/btpd/btpd.c b/btpd/btpd.c
index 8716001..f2b9c00 100644
--- a/btpd/btpd.c
+++ b/btpd/btpd.c
@@ -201,6 +201,7 @@ btpd_init(void)
     ipc_init();
     ul_init();
     cm_init();
+    tlib_init();
 
     signal(SIGPIPE, SIG_IGN);
 
diff --git a/btpd/btpd.h b/btpd/btpd.h
index 55f235f..481c365 100644
--- a/btpd/btpd.h
+++ b/btpd/btpd.h
@@ -17,17 +17,22 @@
 
 #include "benc.h"
 #include "metainfo.h"
+#include "subr.h"
 #include "iobuf.h"
+#include "hashtable.h"
 #include "net_buf.h"
 #include "net_types.h"
 #include "net.h"
 #include "peer.h"
+#include "tlib.h"
 #include "torrent.h"
 #include "download.h"
 #include "upload.h"
-#include "subr.h"
 #include "content.h"
 #include "opts.h"
+#define DAEMON
+#include "btpd_if.h"
+#undef DAEMON
 
 #define BTPD_VERSION (PACKAGE_NAME "/" PACKAGE_VERSION)
 
@@ -43,10 +48,15 @@ extern long btpd_seconds;
 
 void btpd_init(void);
 
+__attribute__((format (printf, 2, 3)))
 void btpd_log(uint32_t type, const char *fmt, ...);
+
+__attribute__((format (printf, 1, 2), noreturn))
 void btpd_err(const char *fmt, ...);
 
+__attribute__((malloc))
 void *btpd_malloc(size_t size);
+__attribute__((malloc))
 void *btpd_calloc(size_t nmemb, size_t size);
 
 void btpd_ev_add(struct event *ev, struct timeval *tv);
diff --git a/btpd/cli_if.c b/btpd/cli_if.c
index a6bde4f..3e71239 100644
--- a/btpd/cli_if.c
+++ b/btpd/cli_if.c
@@ -22,13 +22,6 @@ struct cli {
 
 static struct event m_cli_incoming;
 
-enum ipc_code { // XXX: Same as in cli/btpd_if.h
-    IPC_OK,
-    IPC_FAIL,
-    IPC_ERROR,
-    IPC_COMMERR
-};
-
 static int
 write_buffer(struct cli *cli, struct io_buffer *iob)
 {
@@ -45,7 +38,7 @@ write_buffer(struct cli *cli, struct io_buffer *iob)
 }
 
 static int
-write_code_buffer(struct cli *cli, enum ipc_code code)
+write_code_buffer(struct cli *cli, enum ipc_err code)
 {
     struct io_buffer iob;
     buf_init(&iob, 16);
@@ -54,111 +47,314 @@ write_code_buffer(struct cli *cli, enum ipc_code code)
 }
 
 static int
-cmd_stat(struct cli *cli, int argc, const char *args)
+write_add_buffer(struct cli *cli, unsigned num)
+{
+    struct io_buffer iob;
+    buf_init(&iob, 32);
+    buf_print(&iob, "d4:codei%ue3:numi%uee", IPC_OK, num);
+    return write_buffer(cli, &iob);
+}
+
+static void
+write_ans(struct io_buffer *iob, struct tlib *tl, enum ipc_tval val)
+{
+    enum ipc_tstate ts = IPC_TSTATE_INACTIVE;
+    switch (val) {
+    case IPC_TVAL_CGOT:
+        if (tl->tp == NULL)
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ETINACTIVE);
+        else
+            buf_print(iob, "i%dei%llde", IPC_TYPE_NUM,
+                (long long)cm_content(tl->tp));
+        return;
+    case IPC_TVAL_CSIZE:
+        if (tl->tp == NULL)
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ETINACTIVE);
+        else
+            buf_print(iob, "i%dei%llde", IPC_TYPE_NUM,
+                (long long)tl->tp->total_length);
+        return;
+    case IPC_TVAL_PCCOUNT:
+        if (tl->tp == NULL)
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ETINACTIVE);
+        else
+            buf_print(iob, "i%dei%lue", IPC_TYPE_NUM,
+                (unsigned long)tl->tp->npieces);
+        return;
+    case IPC_TVAL_PCGOT:
+        if (tl->tp == NULL)
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ETINACTIVE);
+        else
+            buf_print(iob, "i%dei%lue", IPC_TYPE_NUM,
+                (unsigned long)cm_pieces(tl->tp));
+        return;
+    case IPC_TVAL_PCSEEN:
+        if (tl->tp == NULL)
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ETINACTIVE);
+        else {
+            unsigned long pcseen = 0;
+            for (unsigned long i = 0; i < tl->tp->npieces; i++)
+                if (tl->tp->net->piece_count[i] > 0)
+                    pcseen++;
+            buf_print(iob, "i%dei%lue", IPC_TYPE_NUM, pcseen);
+        }
+        return;
+    case IPC_TVAL_RATEDWN:
+        if (tl->tp == NULL)
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ETINACTIVE);
+        else
+            buf_print(iob, "i%dei%lue", IPC_TYPE_NUM, tl->tp->net->rate_dwn);
+        return;
+    case IPC_TVAL_RATEUP:
+        if (tl->tp == NULL)
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ETINACTIVE);
+        else
+            buf_print(iob, "i%dei%lue", IPC_TYPE_NUM, tl->tp->net->rate_up);
+        return;
+    case IPC_TVAL_SESSDWN:
+        if (tl->tp == NULL)
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ETINACTIVE);
+        else
+            buf_print(iob, "i%dei%llde", IPC_TYPE_NUM,
+                tl->tp->net->downloaded);
+        return;
+    case IPC_TVAL_SESSUP:
+        if (tl->tp == NULL)
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ETINACTIVE);
+        else
+            buf_print(iob, "i%dei%llde", IPC_TYPE_NUM, tl->tp->net->uploaded);
+        return;
+    case IPC_TVAL_DIR:
+        if (tl->dir != NULL)
+            buf_print(iob, "i%de%d:%s", IPC_TYPE_STR, (int)strlen(tl->dir),
+                tl->dir);
+        else
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_EBADTENT);
+        return;
+    case IPC_TVAL_NAME:
+        if (tl->name != NULL)
+            buf_print(iob, "i%de%d:%s", IPC_TYPE_STR, (int)strlen(tl->name),
+                tl->name);
+        else
+            buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_EBADTENT);
+        return;
+    case IPC_TVAL_IHASH:
+        buf_print(iob, "i%de20:", IPC_TYPE_BIN);
+        buf_write(iob, tl->hash, 20);
+        return;
+    case IPC_TVAL_NUM:
+        buf_print(iob, "i%dei%ue", IPC_TYPE_NUM, tl->num);
+        return;
+    case IPC_TVAL_PCOUNT:
+        buf_print(iob, "i%dei%ue", IPC_TYPE_NUM,
+            tl->tp == NULL ? 0 : tl->tp->net->npeers);
+        return;
+    case IPC_TVAL_STATE:
+        buf_print(iob, "i%de", IPC_TYPE_NUM);
+        if (tl->tp != NULL) {
+            switch (tl->tp->state) {
+            case T_STARTING:
+                ts = IPC_TSTATE_START;
+                break;
+            case T_STOPPING:
+                ts= IPC_TSTATE_STOP;
+                break;
+            case T_ACTIVE:
+                if (cm_full(tl->tp))
+                    ts = IPC_TSTATE_SEED;
+                else
+                    ts = IPC_TSTATE_LEECH;
+                break;
+            }
+        }
+        buf_print(iob, "i%de", ts);
+        return;
+    case IPC_TVAL_TRERR:
+        buf_print(iob, "i%dei%ue", IPC_TYPE_NUM,
+            tl->tp == NULL ? 0 : tr_errors(tl->tp));
+        return;
+    case IPC_TVALCOUNT:
+        break;
+    }
+    buf_print(iob, "i%dei%de", IPC_TYPE_ERR, IPC_ENOKEY);
+}
+
+static int
+cmd_tget(struct cli *cli, int argc, const char *args)
 {
-    struct torrent *tp;
+    if (argc != 1 || !benc_isdct(args))
+        return IPC_COMMERR;
+
+    size_t nkeys;
+    const char *keys, *p;
+    enum ipc_tval *opts;
     struct io_buffer iob;
-    buf_init(&iob, (1 << 14));
-
-    buf_swrite(&iob, "d");
-    buf_swrite(&iob, "4:codei0e");
-    buf_print(&iob, "6:npeersi%ue", net_npeers);
-    buf_print(&iob, "9:ntorrentsi%ue", torrent_count());
-    buf_swrite(&iob, "8:torrentsl");
-    BTPDQ_FOREACH(tp, torrent_get_all(), entry) {
-        const char *name = torrent_name(tp);
-        uint32_t seen_npieces = 0;
-        for (uint32_t i = 0; i < tp->meta.npieces; i++)
-            if (tp->net->piece_count[i] > 0)
-                seen_npieces++;
-
-        buf_swrite(&iob, "d");
-        buf_print(&iob, "11:content goti%llde", (long long)cm_content(tp));
-        buf_print(&iob, "12:content sizei%llde",
-            (long long)tp->meta.total_length);
-        buf_print(&iob, "10:downloadedi%llde", tp->net->downloaded);
-        buf_swrite(&iob, "9:info hash20:");
-        buf_write(&iob, tp->meta.info_hash, 20);
-        buf_print(&iob, "4:name%d:%s", (int)strlen(name), name);
-        buf_print(&iob, "5:peersi%ue", tp->net->npeers);
-        buf_print(&iob, "10:pieces goti%ue", cm_pieces(tp));
-        buf_print(&iob, "11:pieces seeni%ue", seen_npieces);
-        buf_print(&iob, "9:rate downi%lue", tp->net->rate_dwn);
-        buf_print(&iob, "7:rate upi%lue", tp->net->rate_up);
-        buf_print(&iob, "5:statei%ue", tp->state);
-        buf_print(&iob, "14:torrent piecesi%ue", tp->meta.npieces);
-        buf_print(&iob, "14:tracker errorsi%ue", tr_errors(tp));
-        buf_print(&iob, "8:uploadedi%llde", tp->net->uploaded);
-        buf_swrite(&iob, "e");
+
+    if ((keys = benc_dget_lst(args, "keys")) == NULL)
+        return IPC_COMMERR;
+
+    nkeys = benc_nelems(keys);
+    opts = btpd_calloc(nkeys, sizeof(*opts));
+
+    p = benc_first(keys);
+    for (int i = 0; i < nkeys; i++)
+        opts[i] = benc_int(p, &p);
+
+    buf_init(&iob, (1 << 15));
+    buf_swrite(&iob, "d4:codei0e6:resultl");
+    p = benc_dget_any(args, "from");
+    if (benc_isint(p)) {
+        enum ipc_twc from = benc_int(p, NULL);
+        struct tlib *tlv[tlib_count()];
+        tlib_put_all(tlv);
+        for (int i = 0; i < sizeof(tlv) / sizeof(tlv[0]); i++) {
+            if ((from == IPC_TWC_ALL ||
+                    (tlv[i]->tp == NULL && from == IPC_TWC_INACTIVE) ||
+                    (tlv[i]->tp != NULL && from == IPC_TWC_ACTIVE))) {
+                buf_swrite(&iob, "l");
+                for (int k = 0; k < nkeys; k++)
+                    write_ans(&iob, tlv[i], opts[k]);
+                buf_swrite(&iob, "e");
+            }
+        }
+    } else if (benc_islst(p)) {
+        for (p = benc_first(p); p != NULL; p = benc_next(p)) {
+            struct tlib *tl = NULL;
+            if (benc_isint(p))
+                tl = tlib_by_num(benc_int(p, NULL));
+            else if (benc_isstr(p) && benc_strlen(p) == 20)
+                tl = tlib_by_hash(benc_mem(p, NULL, NULL));
+            else {
+                free(iob.buf);
+                free(opts);
+                return IPC_COMMERR;
+            }
+            if (tl != NULL) {
+                buf_swrite(&iob, "l");
+                for (int i = 0; i < nkeys; i++)
+                    write_ans(&iob, tl, opts[i]);
+                buf_swrite(&iob, "e");
+            } else
+                buf_print(&iob, "i%de", IPC_ENOTENT);
+        }
     }
     buf_swrite(&iob, "ee");
+    free(opts);
     return write_buffer(cli, &iob);
 }
 
 static int
 cmd_add(struct cli *cli, int argc, const char *args)
+{
+    if (argc != 1 || !benc_isdct(args))
+        return IPC_COMMERR;
+
+    struct tlib *tl;
+    size_t mi_size = 0, csize = 0;
+    const char *mi, *cp;
+    char content[PATH_MAX];
+    uint8_t hash[20];
+
+    if ((mi = benc_dget_mem(args, "torrent", &mi_size)) == NULL)
+        return IPC_COMMERR;
+
+    if (!mi_test(mi, mi_size))
+        return write_code_buffer(cli, IPC_EBADT);
+
+    if ((cp = benc_dget_mem(args, "content", &csize)) == NULL ||
+            csize >= PATH_MAX || csize == 0)
+        return write_code_buffer(cli, IPC_EBADCDIR);
+
+    if (cp[0] != '/')
+        return write_code_buffer(cli, IPC_EBADCDIR);
+    bcopy(cp, content, csize);
+    content[csize] = '\0';
+
+    tl = tlib_by_hash(mi_info_hash(mi, hash));
+    if (tl != NULL)
+        return write_code_buffer(cli, IPC_ETENTEXIST);
+    tl = tlib_add(hash, mi, mi_size, content,
+        benc_dget_str(args, "name", NULL));
+    return write_add_buffer(cli, tl->num);
+}
+
+static int
+cmd_del(struct cli *cli, int argc, const char *args)
 {
     if (argc != 1)
-        return EINVAL;
-    if (btpd_is_stopping())
-        return write_code_buffer(cli, IPC_FAIL);
-
-    size_t hlen;
-    struct torrent *tp;
-    enum ipc_code code = IPC_OK;
-    const uint8_t *hash = benc_dget_mem(args, "hash", &hlen);
-    char *content = benc_dget_str(args, "content", NULL);
-    char *torrent = benc_dget_str(args, "torrent", NULL);
-
-    if (!(hlen == 20 && content != NULL && torrent != NULL)) {
-        code = IPC_COMMERR;
-        goto out;
-    }
-    if ((tp = torrent_get(hash)) != NULL) {
-        code = tp->state == T_STOPPING ? IPC_FAIL : IPC_OK;
-        goto out;
-    }
-    if (torrent_set_links(hash, torrent, content) != 0) {
-        code = IPC_ERROR;
-        goto out;
-    }
-    if (torrent_start(hash) != 0) {
-        code = IPC_ERROR;
-        goto out;
-    }
+        return IPC_COMMERR;
+
+    struct tlib *tl;
+    enum ipc_err code = IPC_OK;
+    if (benc_isstr(args) && benc_strlen(args) == 20)
+        tl = tlib_by_hash(benc_mem(args, NULL, NULL));
+    else if (benc_isint(args))
+        tl = tlib_by_num(benc_int(args, NULL));
+    else
+        return IPC_COMMERR;
 
-    active_add(hash);
+    if (tl == NULL)
+        code = IPC_ENOTENT;
+    else if (tl->tp != NULL)
+        code = IPC_ETACTIVE;
+    else
+        tlib_del(tl);
 
-out:
-    if (content != NULL)
-        free(content);
-    if (torrent != NULL)
-        free(torrent);
+    return write_code_buffer(cli, code);
+}
 
-    if (code == IPC_COMMERR)
-        return EINVAL;
+static int
+cmd_start(struct cli *cli, int argc, const char *args)
+{
+    if (argc != 1)
+        return IPC_COMMERR;
+    if (btpd_is_stopping())
+        return write_code_buffer(cli, IPC_ESHUTDOWN);
+
+    struct tlib *tl;
+    enum ipc_err code = IPC_OK;
+    if (benc_isstr(args) && benc_strlen(args) == 20)
+        tl = tlib_by_hash(benc_mem(args, NULL, NULL));
+    else if (benc_isint(args))
+        tl = tlib_by_num(benc_int(args, NULL));
     else
-        return write_code_buffer(cli, code);
+        return IPC_COMMERR;
+
+    if (tl == NULL)
+        code = IPC_ENOTENT;
+    else if (tl->tp != NULL)
+        code = IPC_ETACTIVE;
+    else
+        if ((code = torrent_start(tl)) == IPC_OK)
+            active_add(tl->hash);
+    return write_code_buffer(cli, code);
 }
 
 static int
-cmd_del(struct cli *cli, int argc, const char *args)
+cmd_stop(struct cli *cli, int argc, const char *args)
 {
-    if (argc != 1 || !benc_isstr(args))
-        return EINVAL;
-
-    size_t hlen;
-    uint8_t *hash = (uint8_t *)benc_mem(args, &hlen, NULL);
-    if (hlen != 20)
-        return EINVAL;
-    // Stopping a torrent may trigger exit so we need to reply before.
-    int ret = write_code_buffer(cli, IPC_OK);
-    struct torrent *tp = torrent_get(hash);
-    if (tp != NULL) {
-        torrent_stop(tp);
-        active_del(hash);
+    if (argc != 1)
+        return IPC_COMMERR;
+
+    struct tlib *tl;
+    if (benc_isstr(args) && benc_strlen(args) == 20)
+        tl = tlib_by_hash(benc_mem(args, NULL, NULL));
+    else if (benc_isint(args))
+        tl = tlib_by_num(benc_int(args, NULL));
+    else
+        return IPC_COMMERR;
+
+    if (tl == NULL)
+        return write_code_buffer(cli, IPC_ENOTENT);
+    else if (tl->tp == NULL)
+        return write_code_buffer(cli, IPC_ETINACTIVE);
+    else  {
+        // Stopping a torrent may trigger exit so we need to reply before.
+        int ret = write_code_buffer(cli, IPC_OK);
+        active_del(tl->hash);
+        torrent_stop(tl->tp);
+        return ret;
     }
-    return ret;
 }
 
 static int
@@ -183,7 +379,9 @@ static struct {
     { "add",    3, cmd_add },
     { "del",    3, cmd_del },
     { "die",    3, cmd_die },
-    { "stat",   4, cmd_stat }
+    { "start",  5, cmd_start },
+    { "stop",   4, cmd_stop },
+    { "tget",   4, cmd_tget }
 };
 
 static int ncmds = sizeof(cmd_table) / sizeof(cmd_table[0]);
diff --git a/btpd/content.c b/btpd/content.c
index 31c526b..e69216f 100644
--- a/btpd/content.c
+++ b/btpd/content.c
@@ -99,15 +99,14 @@ static int
 fd_cb_rd(const char *path, int *fd, void *arg)
 {
     struct torrent *tp = arg;
-    return vopen(fd, O_RDONLY, "torrents/%s/content/%s", tp->relpath, path);
+    return vopen(fd, O_RDONLY, "%s/%s", tp->tl->dir, path);
 }
 
 static int
 fd_cb_wr(const char *path, int *fd, void *arg)
 {
     struct torrent *tp = arg;
-    return vopen(fd, O_RDWR|O_CREAT, "torrents/%s/content/%s", tp->relpath,
-        path);
+    return vopen(fd, O_RDWR|O_CREAT, "%s/%s", tp->tl->dir, path);
 }
 
 static void
@@ -255,7 +254,8 @@ cm_td_cb(void *arg)
         if (cm->active) {
             assert(!op->u.start.cancel);
             if (!cm_full(tp)) {
-                if ((err = bts_open(&cm->wrs, &tp->meta, fd_cb_wr, tp)) != 0)
+                if ((err = bts_open(&cm->wrs, tp->nfiles, tp->files,
+                        fd_cb_wr, tp)) != 0)
                     btpd_err("Couldn't open write stream for '%s' (%s).\n",
                         torrent_name(tp), strerror(err));
                 btpd_ev_add(&cm->save_timer, SAVE_INTERVAL);
@@ -265,7 +265,7 @@ cm_td_cb(void *arg)
         break;
     case CM_TEST:
         if (op->u.test.ok) {
-            assert(cm->npieces_got < tp->meta.npieces);
+            assert(cm->npieces_got < tp->npieces);
             cm->npieces_got++;
             set_bit(cm->piece_field, op->u.test.piece);
             if (net_active(tp))
@@ -294,13 +294,13 @@ cm_td_cb(void *arg)
 void
 cm_create(struct torrent *tp)
 {
-    size_t pfield_size = ceil(tp->meta.npieces / 8.0);
+    size_t pfield_size = ceil(tp->npieces / 8.0);
     struct content *cm = btpd_calloc(1, sizeof(*cm));
-    cm->bppbf = ceil((double)tp->meta.piece_length / (1 << 17));
+    cm->bppbf = ceil((double)tp->piece_length / (1 << 17));
     cm->piece_field = btpd_calloc(pfield_size, 1);
     cm->hold_field = btpd_calloc(pfield_size, 1);
     cm->pos_field = btpd_calloc(pfield_size, 1);
-    cm->block_field = btpd_calloc(tp->meta.npieces * cm->bppbf, 1);
+    cm->block_field = btpd_calloc(tp->npieces * cm->bppbf, 1);
 
     BTPDQ_INIT(&cm->todoq);
     evtimer_set(&cm->save_timer, save_timer_cb, tp);
@@ -313,7 +313,7 @@ cm_start(struct torrent *tp)
 {
     struct content *cm = tp->cm;
 
-    if ((errno = bts_open(&cm->rds, &tp->meta, fd_cb_rd, tp)) != 0)
+    if ((errno = bts_open(&cm->rds, tp->nfiles, tp->files, fd_cb_rd, tp)) != 0)
         btpd_err("Error opening stream (%s).\n", strerror(errno));
 
     cm->active = 1;
@@ -329,7 +329,7 @@ cm_get_bytes(struct torrent *tp, uint32_t piece, uint32_t begin, size_t len,
 {
     *buf = btpd_malloc(len);
     int err =
-        bts_get(tp->cm->rds, piece * tp->meta.piece_length + begin, *buf, len);
+        bts_get(tp->cm->rds, piece * tp->piece_length + begin, *buf, len);
     if (err != 0)
         btpd_err("Io error (%s)\n", strerror(err));
     return 0;
@@ -365,9 +365,9 @@ cm_prealloc(struct torrent *tp, uint32_t piece)
     if (cm_alloc_size <= 0)
         set_bit(cm->pos_field, piece);
     else {
-        unsigned npieces = ceil((double)cm_alloc_size / tp->meta.piece_length);
+        unsigned npieces = ceil((double)cm_alloc_size / tp->piece_length);
         uint32_t start = piece - piece % npieces;
-        uint32_t end = min(start + npieces, tp->meta.npieces);
+        uint32_t end = min(start + npieces, tp->npieces);
 
         while (start < end) {
             if ((!has_bit(cm->pos_field, start)
@@ -416,7 +416,7 @@ cm_put_bytes(struct torrent *tp, uint32_t piece, uint32_t begin,
         if (it == NULL)
             BTPDQ_INSERT_TAIL(&op->u.write.q, d, entry);
     } else {
-        err = bts_put(cm->wrs, piece * tp->meta.piece_length + begin, buf,
+        err = bts_put(cm->wrs, piece * tp->piece_length + begin, buf,
             len);
         if (err != 0)
             btpd_err("Io error (%s)\n", strerror(err));
@@ -432,7 +432,7 @@ cm_put_bytes(struct torrent *tp, uint32_t piece, uint32_t begin,
 int
 cm_full(struct torrent *tp)
 {
-    return tp->cm->npieces_got == tp->meta.npieces;
+    return tp->cm->npieces_got == tp->npieces;
 }
 
 off_t
@@ -468,23 +468,19 @@ cm_has_piece(struct torrent *tp, uint32_t piece)
 static int
 test_hash(struct torrent *tp, uint8_t *hash, uint32_t piece)
 {
-    if (tp->meta.piece_hash != NULL)
-        return bcmp(hash, tp->meta.piece_hash[piece], SHA_DIGEST_LENGTH);
-    else {
-        char piece_hash[SHA_DIGEST_LENGTH];
-        int fd;
-        int err;
+    char piece_hash[SHA_DIGEST_LENGTH];
+    int fd;
+    int err;
 
-        err = vopen(&fd, O_RDONLY, "torrents/%s/torrent", tp->relpath);
-        if (err != 0)
-            btpd_err("test_hash: %s\n", strerror(err));
+    err = vopen(&fd, O_RDONLY, "torrents/%s/torrent", tp->relpath);
+    if (err != 0)
+        btpd_err("test_hash: %s\n", strerror(err));
 
-        lseek(fd, tp->meta.pieces_off + piece * SHA_DIGEST_LENGTH, SEEK_SET);
-        read(fd, piece_hash, SHA_DIGEST_LENGTH);
-        close(fd);
+    lseek(fd, tp->pieces_off + piece * SHA_DIGEST_LENGTH, SEEK_SET);
+    read(fd, piece_hash, SHA_DIGEST_LENGTH);
+    close(fd);
 
-        return bcmp(hash, piece_hash, SHA_DIGEST_LENGTH);
-    }
+    return bcmp(hash, piece_hash, SHA_DIGEST_LENGTH);
 }
 
 static int
@@ -493,9 +489,9 @@ test_piece(struct torrent *tp, uint32_t pos, uint32_t piece, int *ok)
     int err;
     uint8_t hash[SHA_DIGEST_LENGTH];
     struct bt_stream *bts;
-    if ((err = bts_open(&bts, &tp->meta, fd_cb_rd, tp)) != 0)
+    if ((err = bts_open(&bts, tp->nfiles, tp->files, fd_cb_rd, tp)) != 0)
         return err;
-    if ((err = bts_sha(bts, pos * tp->meta.piece_length,
+    if ((err = bts_sha(bts, pos * tp->piece_length,
              torrent_piece_size(tp, piece), hash)) != 0)
         return err;;
     bts_close(bts);
@@ -514,11 +510,11 @@ cm_td_alloc(struct cm_op *op)
 
     assert(!has_bit(cm->pos_field, pos));
 
-    if ((err = bts_open(&bts, &tp->meta, fd_cb_wr, tp)) != 0)
+    if ((err = bts_open(&bts, tp->nfiles, tp->files, fd_cb_wr, tp)) != 0)
         goto out;
 
     off_t len = torrent_piece_size(tp, pos);
-    off_t off = tp->meta.piece_length * pos;
+    off_t off = tp->piece_length * pos;
     while (len > 0) {
         size_t wlen = min(ZEROBUFLEN, len);
         if ((err = bts_put(bts, off, m_zerobuf, wlen)) != 0) {
@@ -545,22 +541,20 @@ test_torrent(struct torrent *tp, volatile sig_atomic_t *cancel)
     if ((err = vfopen(&fp, "r", "torrents/%s/torrent", tp->relpath)) != 0)
         return err;
 
-    hashes = btpd_malloc(tp->meta.npieces * SHA_DIGEST_LENGTH);
-    fseek(fp, tp->meta.pieces_off, SEEK_SET);
-    fread(hashes, SHA_DIGEST_LENGTH, tp->meta.npieces, fp);
+    hashes = btpd_malloc(tp->npieces * SHA_DIGEST_LENGTH);
+    fseek(fp, tp->pieces_off, SEEK_SET);
+    fread(hashes, SHA_DIGEST_LENGTH, tp->npieces, fp);
     fclose(fp);
 
-    tp->meta.piece_hash = hashes;
-
     struct content *cm = tp->cm;
-    for (uint32_t piece = 0; piece < tp->meta.npieces; piece++) {
+    for (uint32_t piece = 0; piece < tp->npieces; piece++) {
         if (!has_bit(cm->pos_field, piece))
             continue;
-        err = bts_sha(cm->rds, piece * tp->meta.piece_length,
+        err = bts_sha(cm->rds, piece * tp->piece_length,
             torrent_piece_size(tp, piece), hash);
         if (err != 0)
             break;
-        if (test_hash(tp, hash, piece) == 0)
+        if (bcmp(hashes[piece], hash, SHA_DIGEST_LENGTH) == 0)
             set_bit(tp->cm->piece_field, piece);
         else
             clear_bit(tp->cm->piece_field, piece);
@@ -570,7 +564,6 @@ test_torrent(struct torrent *tp, volatile sig_atomic_t *cancel)
         }
     }
 
-    tp->meta.piece_hash = NULL;
     free(hashes);
     return err;
 }
@@ -585,9 +578,8 @@ stat_and_adjust(struct torrent *tp, struct rstat ret[])
 {
     char path[PATH_MAX];
     struct stat sb;
-    for (int i = 0; i < tp->meta.nfiles; i++) {
-        snprintf(path, PATH_MAX, "torrents/%s/content/%s", tp->relpath,
-            tp->meta.files[i].path);
+    for (int i = 0; i < tp->nfiles; i++) {
+        snprintf(path, PATH_MAX, "%s/%s", tp->tl->dir, tp->files[i].path);
 again:
         if (stat(path, &sb) == -1) {
             if (errno == ENOENT) {
@@ -599,8 +591,8 @@ again:
             ret[i].mtime = sb.st_mtime;
             ret[i].size = sb.st_size;
         }
-        if (ret[i].size > tp->meta.files[i].length) {
-            if (truncate(path, tp->meta.files[i].length) != 0)
+        if (ret[i].size > tp->files[i].length) {
+            if (truncate(path, tp->files[i].length) != 0)
                 return errno;
             goto again;
         }
@@ -613,8 +605,8 @@ load_resume(struct torrent *tp, struct rstat sbs[])
 {
     int err, ver;
     FILE *fp;
-    size_t pfsiz = ceil(tp->meta.npieces / 8.0);
-    size_t bfsiz = tp->meta.npieces * tp->cm->bppbf;
+    size_t pfsiz = ceil(tp->npieces / 8.0);
+    size_t bfsiz = tp->npieces * tp->cm->bppbf;
 
     if ((err = vfopen(&fp, "r" , "torrents/%s/resume", tp->relpath)) != 0)
         return err;
@@ -623,7 +615,7 @@ load_resume(struct torrent *tp, struct rstat sbs[])
         goto invalid;
     if (ver != 1)
         goto invalid;
-    for (int i = 0; i < tp->meta.nfiles; i++) {
+    for (int i = 0; i < tp->nfiles; i++) {
         quad_t size;
         long time;
         if (fscanf(fp, "%qd %ld\n", &size, &time) != 2)
@@ -652,10 +644,10 @@ save_resume(struct torrent *tp, struct rstat sbs[])
     if ((err = vfopen(&fp, "wb", "torrents/%s/resume", tp->relpath)) != 0)
         return err;
     fprintf(fp, "%d\n", 1);
-    for (int i = 0; i < tp->meta.nfiles; i++)
+    for (int i = 0; i < tp->nfiles; i++)
         fprintf(fp, "%lld %ld\n", (long long)sbs[i].size, (long)sbs[i].mtime);
-    fwrite(tp->cm->piece_field, 1, ceil(tp->meta.npieces / 8.0), fp);
-    fwrite(tp->cm->block_field, 1, tp->meta.npieces * tp->cm->bppbf, fp);
+    fwrite(tp->cm->piece_field, 1, ceil(tp->npieces / 8.0), fp);
+    fwrite(tp->cm->block_field, 1, tp->npieces * tp->cm->bppbf, fp);
     if (fclose(fp) != 0)
         err = errno;
     return err;
@@ -665,7 +657,7 @@ static void
 cm_td_save(struct cm_op *op)
 {
     struct torrent *tp = op->tp;
-    struct rstat sbs[tp->meta.nfiles];
+    struct rstat sbs[tp->nfiles];
     if (stat_and_adjust(tp, sbs) == 0)
         save_resume(tp, sbs);
 }
@@ -674,7 +666,7 @@ static void
 cm_td_start(struct cm_op *op)
 {
     int err, resume_clean = 0, tested_torrent = 0;
-    struct rstat sbs[op->tp->meta.nfiles];
+    struct rstat sbs[op->tp->nfiles];
     struct torrent *tp = op->tp;
     struct content *cm = tp->cm;
 
@@ -683,17 +675,17 @@ cm_td_start(struct cm_op *op)
 
     resume_clean = load_resume(tp, sbs) == 0;
     if (!resume_clean) {
-        memset(cm->pos_field, 0xff, ceil(tp->meta.npieces / 8.0));
+        memset(cm->pos_field, 0xff, ceil(tp->npieces / 8.0));
         off_t off = 0;
-        for (int i = 0; i < tp->meta.nfiles; i++) {
-            if (sbs[i].size != tp->meta.files[i].length) {
+        for (int i = 0; i < tp->nfiles; i++) {
+            if (sbs[i].size != tp->files[i].length) {
                 uint32_t start, end;
-                end = (off + tp->meta.files[i].length - 1)
-                    / tp->meta.piece_length;
+                end = (off + tp->files[i].length - 1)
+                    / tp->piece_length;
                 if (sbs[i].size == -1)
-                    start = off / tp->meta.piece_length;
+                    start = off / tp->piece_length;
                 else
-                    start = (off + sbs[i].size) / tp->meta.piece_length;
+                    start = (off + sbs[i].size) / tp->piece_length;
                 while (start <= end) {
                     clear_bit(cm->pos_field, start);
                     clear_bit(cm->piece_field, start);
@@ -701,7 +693,7 @@ cm_td_start(struct cm_op *op)
                     start++;
                 }
             }
-            off += tp->meta.files[i].length;
+            off += tp->files[i].length;
         }
         if (op->u.start.cancel)
             goto out;
@@ -710,8 +702,8 @@ cm_td_start(struct cm_op *op)
         tested_torrent = 1;
     }
 
-    bzero(cm->pos_field, ceil(tp->meta.npieces / 8.0));
-    for (uint32_t piece = 0; piece < tp->meta.npieces; piece++) {
+    bzero(cm->pos_field, ceil(tp->npieces / 8.0));
+    for (uint32_t piece = 0; piece < tp->npieces; piece++) {
         if (cm_has_piece(tp, piece)) {
             cm->ncontent_bytes += torrent_piece_size(tp, piece);
             cm->npieces_got++;
@@ -768,9 +760,10 @@ cm_td_write(struct cm_op *op)
 {
     int err;
     struct cm_write_data *d, *next;
-    off_t base = op->tp->meta.piece_length * op->u.write.pos;
+    off_t base = op->tp->piece_length * op->u.write.pos;
     struct bt_stream *bts;
-    if ((err = bts_open(&bts, &op->tp->meta, fd_cb_wr, op->tp)) != 0)
+    if ((err = bts_open(&bts, op->tp->nfiles, op->tp->files,
+            fd_cb_wr, op->tp)) != 0)
         goto out;
     BTPDQ_FOREACH(d, &op->u.write.q, entry)
         if ((err = bts_put(bts, base + d->begin, d->buf, d->len)) != 0) {
diff --git a/btpd/download.c b/btpd/download.c
index 9ef54be..a836ac9 100644
--- a/btpd/download.c
+++ b/btpd/download.c
@@ -148,7 +148,7 @@ dl_on_lost_peer(struct peer *p)
 {
     struct net *n = p->n;
 
-    for (uint32_t i = 0; i < n->tp->meta.npieces; i++)
+    for (uint32_t i = 0; i < n->tp->npieces; i++)
         if (peer_has(p, i))
             n->piece_count[i]--;
 
diff --git a/btpd/download_subr.c b/btpd/download_subr.c
index a38e553..d12efa9 100644
--- a/btpd/download_subr.c
+++ b/btpd/download_subr.c
@@ -33,7 +33,7 @@ static struct piece *
 piece_alloc(struct net *n, uint32_t index)
 {
     assert(!has_bit(n->busy_field, index)
-        && n->npcs_busy < n->tp->meta.npieces);
+        && n->npcs_busy < n->tp->npieces);
     struct piece *pc;
     size_t mem, field;
     unsigned nblocks;
@@ -98,7 +98,7 @@ static int
 dl_should_enter_endgame(struct net *n)
 {
     int should;
-    if (cm_pieces(n->tp) + n->npcs_busy == n->tp->meta.npieces) {
+    if (cm_pieces(n->tp) + n->npcs_busy == n->tp->npieces) {
         should = 1;
         struct piece *pc;
         BTPDQ_FOREACH(pc, &n->getlst, entry) {
@@ -216,15 +216,15 @@ dl_choose_rarest(struct peer *p, uint32_t *res)
 
     assert(n->endgame == 0);
 
-    for (i = 0; i < n->tp->meta.npieces && !dl_piece_startable(p, i); i++)
+    for (i = 0; i < n->tp->npieces && !dl_piece_startable(p, i); i++)
         ;
 
-    if (i == n->tp->meta.npieces)
+    if (i == n->tp->npieces)
         return ENOENT;
 
     uint32_t min_i = i;
     uint32_t min_c = 1;
-    for(i++; i < n->tp->meta.npieces; i++) {
+    for(i++; i < n->tp->npieces; i++) {
         if (dl_piece_startable(p, i)) {
             if (n->piece_count[i] == n->piece_count[min_i])
                 min_c++;
diff --git a/btpd/net.c b/btpd/net.c
index 37307d6..02171ed 100644
--- a/btpd/net.c
+++ b/btpd/net.c
@@ -53,9 +53,9 @@ net_torrent_has_peer(struct net *n, const uint8_t *id)
 void
 net_create(struct torrent *tp)
 {
-    size_t field_size = ceil(tp->meta.npieces / 8.0);
+    size_t field_size = ceil(tp->npieces / 8.0);
     size_t mem = sizeof(*(tp->net)) + field_size +
-        tp->meta.npieces * sizeof(*(tp->net->piece_count));
+        tp->npieces * sizeof(*(tp->net->piece_count));
 
     struct net *n = btpd_calloc(1, mem);
     n->tp = tp;
@@ -260,7 +260,7 @@ net_dispatch_msg(struct peer *p, const char *buf)
             begin = net_read32(buf + 4);
             length = net_read32(buf + 8);
             if ((length > PIECE_BLOCKLEN
-                    || index >= p->n->tp->meta.npieces
+                    || index >= p->n->tp->npieces
                     || !cm_has_piece(p->n->tp, index)
                     || begin + length > torrent_piece_size(p->n->tp, index))) {
                 btpd_log(BTPD_L_MSG, "bad request: (%u, %u, %u) from %p\n",
@@ -300,7 +300,7 @@ net_mh_ok(struct peer *p)
     case MSG_HAVE:
         return mlen == 5;
     case MSG_BITFIELD:
-        return mlen == (uint32_t)ceil(p->n->tp->meta.npieces / 8.0) + 1;
+        return mlen == (uint32_t)ceil(p->n->tp->npieces / 8.0) + 1;
     case MSG_REQUEST:
     case MSG_CANCEL:
         return mlen == 13;
@@ -331,15 +331,12 @@ net_state(struct peer *p, const char *buf)
         break;
     case SHAKE_INFO:
         if (p->flags & PF_INCOMING) {
-            struct net *n;
-            BTPDQ_FOREACH(n, &m_torrents, entry)
-                if (bcmp(buf, n->tp->meta.info_hash, 20) == 0)
-                    break;
-            if (n == NULL)
+            struct torrent *tp = torrent_by_hash(buf);
+            if (tp == NULL || tp->net == NULL)
                 goto bad;
-            p->n = n;
+            p->n = tp->net;
             peer_send(p, nb_create_shake(p->n->tp));
-        } else if (bcmp(buf, p->n->tp->meta.info_hash, 20) != 0)
+        } else if (bcmp(buf, p->n->tp->tl->hash, 20) != 0)
             goto bad;
         peer_set_in_state(p, SHAKE_ID, 20);
         break;
diff --git a/btpd/net_buf.c b/btpd/net_buf.c
index 5b09ddb..6827c9c 100644
--- a/btpd/net_buf.c
+++ b/btpd/net_buf.c
@@ -184,7 +184,7 @@ nb_create_interest(void)
 struct net_buf *
 nb_create_bitfield(struct torrent *tp)
 {
-    uint32_t plen = ceil(tp->meta.npieces / 8.0);
+    uint32_t plen = ceil(tp->npieces / 8.0);
 
     struct net_buf *out = nb_create_alloc(NB_BITFIELD, 5);
     net_write32(out->buf, plen + 1);
@@ -195,7 +195,7 @@ nb_create_bitfield(struct torrent *tp)
 struct net_buf *
 nb_create_bitdata(struct torrent *tp)
 {
-    uint32_t plen = ceil(tp->meta.npieces / 8.0);
+    uint32_t plen = ceil(tp->npieces / 8.0);
     struct net_buf *out =
         nb_create_set(NB_BITDATA, cm_get_piece_field(tp), plen, kill_buf_no);
     return out;
@@ -206,7 +206,7 @@ nb_create_shake(struct torrent *tp)
 {
     struct net_buf *out = nb_create_alloc(NB_SHAKE, 68);
     bcopy("\x13""BitTorrent protocol\0\0\0\0\0\0\0\0", out->buf, 28);
-    bcopy(tp->meta.info_hash, out->buf + 28, 20);
+    bcopy(tp->tl->hash, out->buf + 28, 20);
     bcopy(btpd_get_peer_id(), out->buf + 48, 20);
     return out;
 }
diff --git a/btpd/peer.c b/btpd/peer.c
index 714489d..5731e05 100644
--- a/btpd/peer.c
+++ b/btpd/peer.c
@@ -357,10 +357,10 @@ peer_on_shake(struct peer *p)
         printid[i] = p->id[i];
     printid[i] = '\0';
     btpd_log(BTPD_L_MSG, "received shake(%s) from %p\n", printid, p);
-    p->piece_field = btpd_calloc(1, (int)ceil(p->n->tp->meta.npieces / 8.0));
+    p->piece_field = btpd_calloc(1, (int)ceil(p->n->tp->npieces / 8.0));
     if (cm_pieces(p->n->tp) > 0) {
         if ((cm_pieces(p->n->tp) * 9 < 5 +
-                ceil(p->n->tp->meta.npieces / 8.0)))
+                ceil(p->n->tp->npieces / 8.0)))
             peer_send(p, nb_create_multihave(p->n->tp));
         else {
             peer_send(p, nb_create_bitfield(p->n->tp));
@@ -449,8 +449,8 @@ peer_on_bitfield(struct peer *p, const uint8_t *field)
 {
     btpd_log(BTPD_L_MSG, "received bitfield from %p\n", p);
     assert(p->npieces == 0);
-    bcopy(field, p->piece_field, (size_t)ceil(p->n->tp->meta.npieces / 8.0));
-    for (uint32_t i = 0; i < p->n->tp->meta.npieces; i++) {
+    bcopy(field, p->piece_field, (size_t)ceil(p->n->tp->npieces / 8.0));
+    for (uint32_t i = 0; i < p->n->tp->npieces; i++) {
         if (has_bit(p->piece_field, i)) {
             p->npieces++;
             dl_on_piece_ann(p, i);
@@ -593,5 +593,5 @@ peer_active_up(struct peer *p)
 int
 peer_full(struct peer *p)
 {
-    return p->npieces == p->n->tp->meta.npieces;
+    return p->npieces == p->n->tp->npieces;
 }
diff --git a/btpd/tlib.c b/btpd/tlib.c
new file mode 100644
index 0000000..812a141
--- /dev/null
+++ b/btpd/tlib.c
@@ -0,0 +1,246 @@
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <dirent.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "btpd.h"
+
+HTBL_TYPE(numtbl, tlib, unsigned, num, nchain);
+HTBL_TYPE(hashtbl, tlib, uint8_t, hash, hchain);
+
+static unsigned m_nextnum;
+static unsigned m_ntlibs;
+static struct numtbl *m_numtbl;
+static struct hashtbl *m_hashtbl;
+
+unsigned
+tlib_count(void)
+{
+    return m_ntlibs;
+}
+
+struct tlib *
+tlib_by_num(unsigned num)
+{
+    return numtbl_find(m_numtbl, &num);
+}
+
+struct tlib *
+tlib_by_hash(const uint8_t *hash)
+{
+    return hashtbl_find(m_hashtbl, hash);
+}
+
+void
+tlib_kill(struct tlib *tl)
+{
+    numtbl_remove(m_numtbl, &tl->num);
+    hashtbl_remove(m_hashtbl, tl->hash);
+    free(tl);
+    m_ntlibs--;
+}
+
+struct tlib *
+tlib_create(const uint8_t *hash)
+{
+    struct tlib *tl = btpd_calloc(1, sizeof(*tl));
+    char hex[SHAHEXSIZE];
+    bin2hex(hash, hex, 20);
+    tl->num = m_nextnum;
+    bcopy(hash, tl->hash, 20);
+    m_nextnum++;
+    m_ntlibs++;
+    numtbl_insert(m_numtbl, tl);
+    hashtbl_insert(m_hashtbl, tl);
+    return tl;
+}
+
+int
+tlib_del(struct tlib *tl)
+{
+    char relpath[RELPATH_SIZE];
+    char cmd[PATH_MAX];
+    assert(tl->tp == NULL);
+    snprintf(cmd, PATH_MAX, "rm -r torrents/%s",
+        bin2hex(tl->hash, relpath, 20));
+    system(cmd);
+    tlib_kill(tl);
+    return 0;
+}
+
+static int
+valid_info(char *buf, size_t len)
+{
+    size_t slen;
+    if (benc_validate(buf, len) != 0)
+        return 0;
+    if (benc_dget_mem(buf, "name", &slen) == NULL || slen == 0)
+        return 0;
+    if ((benc_dget_mem(buf, "dir", &slen) == NULL ||
+            (slen == 0 || slen >= PATH_MAX)))
+        return 0;
+    return 1;
+}
+
+static void
+load_info(struct tlib *tl, const char *path)
+{
+    size_t size = 1 << 14;
+    char buf[size], *p = buf;
+
+    if ((errno = read_whole_file((void **)&p, &size, path)) != 0) {
+        btpd_log(BTPD_L_ERROR, "couldn't load '%s' (%s).\n", path,
+            strerror(errno));
+        return;
+    }
+
+    if (!valid_info(buf, size)) {
+        btpd_log(BTPD_L_ERROR, "bad info file '%s'.\n", path);
+        return ;
+    }
+
+    tl->name = benc_dget_str(buf, "name", NULL);
+    tl->dir = benc_dget_str(buf, "dir", NULL);
+#if 0
+    tl->t_added = benc_dget_int(buf, "time added");
+    tl->t_active = benc_dget_int(buf, "time active");
+    tl->tot_up = benc_dget_int(buf, "total upload");
+    tl->tot_down = benc_dget_int(buf, "total download");
+#endif
+    if (tl->name == NULL || tl->dir == NULL)
+        btpd_err("out of memory.\n");
+}
+
+static void
+save_info(struct tlib *tl, const char *path)
+{
+    FILE *fp;
+    char wpath[PATH_MAX];
+    snprintf(wpath, PATH_MAX, "%s.write", path);
+    if ((fp = fopen(wpath, "w")) == NULL)
+        btpd_err("failed to open '%s' (%s).\n", wpath, strerror(errno));
+    fprintf(fp, "d3:dir%d:%s4:name%d:%s", (int)strlen(tl->dir), tl->dir,
+        (int)strlen(tl->name), tl->name);
+#if 0
+    fprintf(fp, "11:time activei%lde10:time addedi%lde", tl->t_active,
+        tl->t_added);
+    fprintf(fp, "14:total downloadi%llde12:total uploadi%lldee", tl->tot_down,
+        tl->tot_up);
+#else
+    fprintf(fp, "e");
+#endif
+    if (ferror(fp) || fclose(fp) != 0)
+        btpd_err("failed to write '%s'.\n", wpath);
+    if (rename(wpath, path) != 0)
+        btpd_err("failed to rename: '%s' -> '%s' (%s).\n", wpath, path,
+            strerror(errno));
+}
+
+static void
+write_torrent(const char *mi, size_t mi_size, const char *path)
+{
+    FILE *fp;
+    if ((fp = fopen(path, "w")) == NULL)
+        goto err;
+    if (fwrite(mi, mi_size, 1, fp) != 1) {
+        errno = EIO;
+        goto err;
+    }
+    if (fclose(fp) != 0)
+        goto err;
+    return;
+err:
+    btpd_err("failed to write metainfo '%s' (%s).\n", path, strerror(errno));
+}
+
+struct tlib *
+tlib_add(const uint8_t *hash, const char *mi, size_t mi_size,
+    const char *content, char *name)
+{
+    struct tlib *tl = tlib_create(hash);
+#if 0
+    struct timeval tv;
+#endif
+    char relpath[RELPATH_SIZE], file[PATH_MAX];
+    bin2hex(hash, relpath, 20);
+
+    if (name == NULL)
+        if ((name = mi_name(mi)) == NULL)
+            btpd_err("out of memory.\n");
+
+    tl->name = name;
+    tl->dir = strdup(content);
+    if (tl->name == NULL || tl->dir == NULL)
+        btpd_err("out of memory.\n");
+
+#if 0
+    gettimeofday(&tv, NULL);
+    tl->t_added = tv.tv_sec;
+#endif
+
+    snprintf(file, PATH_MAX, "torrents/%s", relpath);
+    if (mkdir(file, 0777) != 0)
+        btpd_err("failed to create dir '%s' (%s).\n", file, strerror(errno));
+    snprintf(file, PATH_MAX, "torrents/%s/torrent", relpath);
+    write_torrent(mi, mi_size, file);
+    snprintf(file, PATH_MAX, "torrents/%s/info", relpath);
+    save_info(tl, file);
+    return tl;
+}
+
+static int
+num_test(const void *k1, const void *k2)
+{
+    return *(const unsigned *)k1 == *(const unsigned *)k2;
+}
+
+static uint32_t
+num_hash(const void *k)
+{
+    return *(const unsigned *)k;
+}
+
+static int
+id_test(const void *k1, const void *k2)
+{
+    return bcmp(k1, k2, 20) == 0;
+}
+
+static uint32_t
+id_hash(const void *k)
+{
+    return net_read32(k + 16);
+}
+
+void
+tlib_put_all(struct tlib **v)
+{
+    hashtbl_tov(m_hashtbl, v);
+}
+
+void
+tlib_init(void)
+{
+    DIR *dirp;
+    struct dirent *dp;
+    uint8_t hash[20];
+    char file[PATH_MAX];
+
+    m_numtbl = numtbl_create(num_test, num_hash);
+    m_hashtbl = hashtbl_create(id_test, id_hash);
+    if (m_numtbl == NULL || m_hashtbl == NULL)
+        btpd_err("Out of memory.\n");
+
+    if ((dirp = opendir("torrents")) == NULL)
+        btpd_err("couldn't open the torrents directory.\n");
+    while ((dp = readdir(dirp)) != NULL) {
+        if (dp->d_namlen == 40 && ishex(dp->d_name)) {
+            struct tlib * tl = tlib_create(hex2bin(dp->d_name, hash, 20));
+            snprintf(file, PATH_MAX, "torrents/%s/info", dp->d_name);
+            load_info(tl, file);
+        }
+    }
+    closedir(dirp);
+}
diff --git a/btpd/tlib.h b/btpd/tlib.h
new file mode 100644
index 0000000..2dc604e
--- /dev/null
+++ b/btpd/tlib.h
@@ -0,0 +1,30 @@
+#ifndef BTPD_TLIB_H
+#define BTPD_TLIB_H
+
+struct tlib {
+    unsigned num;
+    uint8_t hash[20];
+    struct torrent *tp;
+
+    char *name;
+    char *dir;
+#if 0
+    unsigned long long tot_up, tot_down;
+    long t_added, t_active;
+#endif
+    HTBL_ENTRY(nchain);
+    HTBL_ENTRY(hchain);
+};
+
+void tlib_init(void);
+void tlib_put_all(struct tlib **v);
+
+struct tlib *tlib_add(const uint8_t *hash, const char *mi, size_t mi_size,
+    const char *content, char *name);
+int tlib_del(struct tlib *tl);
+
+struct tlib *tlib_by_hash(const uint8_t *hash);
+struct tlib *tlib_by_num(unsigned num);
+unsigned tlib_count(void);
+
+#endif
diff --git a/btpd/torrent.c b/btpd/torrent.c
index db4f7dc..552a5ae 100644
--- a/btpd/torrent.c
+++ b/btpd/torrent.c
@@ -35,28 +35,33 @@ torrent_count(void)
 }
 
 struct torrent *
-torrent_get(const uint8_t *hash)
+torrent_by_num(unsigned num)
 {
-    struct torrent *tp = BTPDQ_FIRST(&m_torrents);
-    while (tp != NULL && bcmp(hash, tp->meta.info_hash, 20) != 0)
-        tp = BTPDQ_NEXT(tp, entry);
-    return tp;
+    struct tlib *tl = tlib_by_num(num);
+    return tl != NULL ? tl->tp : NULL;
+}
+
+struct torrent *
+torrent_by_hash(const uint8_t *hash)
+{
+    struct tlib *tl = tlib_by_hash(hash);
+    return tl != NULL ? tl->tp : NULL;
 }
 
 const char *
 torrent_name(struct torrent *tp)
 {
-    return tp->meta.name;
+    return tp->tl->name;
 }
 
 off_t
 torrent_piece_size(struct torrent *tp, uint32_t index)
 {
-    if (index < tp->meta.npieces - 1)
-        return tp->meta.piece_length;
+    if (index < tp->npieces - 1)
+        return tp->piece_length;
     else {
-        off_t allbutlast = tp->meta.piece_length * (tp->meta.npieces - 1);
-        return tp->meta.total_length - allbutlast;
+        off_t allbutlast = tp->piece_length * (tp->npieces - 1);
+        return tp->total_length - allbutlast;
     }
 }
 
@@ -78,85 +83,84 @@ torrent_block_size(struct torrent *tp, uint32_t piece, uint32_t nblocks,
     }
 }
 
-static void
-torrent_relpath(const uint8_t *hash, char *buf)
-{
-    for (int i = 0; i < 20; i++)
-        snprintf(buf + i * 2, 3, "%.2x", hash[i]);
-}
-
-int
-torrent_set_links(const uint8_t *hash, const char *torrent,
-    const char *content)
-{
-    char relpath[RELPATH_SIZE];
-    char file[PATH_MAX];
-    torrent_relpath(hash, relpath);
-    snprintf(file, PATH_MAX, "torrents/%s", relpath);
-    if (mkdir(file, 0777) == -1 && errno != EEXIST)
-        return errno;
-    snprintf(file, PATH_MAX, "torrents/%s/torrent", relpath);
-    if (unlink(file) == -1 && errno != ENOENT)
-        return errno;
-    if (symlink(torrent, file) == -1)
-        return errno;
-    snprintf(file, PATH_MAX, "torrents/%s/content", relpath);
-    if (unlink(file) == -1 && errno != ENOENT)
-        return errno;
-    if (symlink(content, file) == -1)
-        return errno;
-    return 0;
-}
-
-int
-torrent_start(const uint8_t *hash)
+enum ipc_err
+torrent_start(struct tlib *tl)
 {
+    struct stat sb;
     struct torrent *tp;
-    struct metainfo *mi;
-    int error;
+    char *mi;
     char relpath[RELPATH_SIZE];
     char file[PATH_MAX];
 
-    torrent_relpath(hash, relpath);
-    snprintf(file, PATH_MAX, "torrents/%s/torrent", relpath);
+    if (tl->dir == NULL)
+        return IPC_EBADTENT;
+
+    if (mkdir(tl->dir, 0777) != 0 && errno != EEXIST) {
+        btpd_log(BTPD_L_ERROR, "torrent '%s': "
+            "failed to create content dir '%s' (%s).\n",
+            tl->name, tl->dir, strerror(errno));
+        return IPC_ECREATECDIR;
+    } else if (stat(tl->dir, &sb) == -1 ||
+            ((sb.st_mode & S_IFMT) != S_IFDIR)) {
+        btpd_log(BTPD_L_ERROR,
+            "torrent '%s': content dir '%s' is either not a directory or"
+            " cannot be accessed.\n", tl->name, tl->dir);
+        return IPC_EBADCDIR;
+    }
 
-    if ((error = load_metainfo(file, -1, 0, &mi)) != 0) {
-        btpd_log(BTPD_L_ERROR, "Couldn't load torrent file %s: %s.\n",
-            file, strerror(error));
-        return error;
+    bin2hex(tl->hash, relpath, 20);
+    snprintf(file, PATH_MAX, "torrents/%s/torrent", relpath);
+    if ((mi = mi_load(file, NULL)) == NULL) {
+        btpd_log(BTPD_L_ERROR,
+            "torrent '%s': failed to load metainfo (%s).\n",
+            tl->name, strerror(errno));
+        return IPC_EBADTENT;
     }
 
     tp = btpd_calloc(1, sizeof(*tp));
+    tp->tl = tl;
     bcopy(relpath, tp->relpath, RELPATH_SIZE);
-    tp->meta = *mi;
-    free(mi);
+    tp->files = mi_files(mi);
+    tp->nfiles = mi_nfiles(mi);
+    if (tp->files == NULL)
+        btpd_err("out of memory.\n");
+    tp->total_length = mi_total_length(mi);
+    tp->piece_length = mi_piece_length(mi);
+    tp->npieces = mi_npieces(mi);
+    tp->pieces_off =
+        benc_dget_mem(benc_dget_dct(mi, "info"), "pieces", NULL) - mi;
 
     btpd_log(BTPD_L_BTPD, "Starting torrent '%s'.\n", torrent_name(tp));
-    if ((error = tr_create(tp)) == 0) {
+    if (tr_create(tp, mi) == 0) {
         net_create(tp);
         cm_create(tp);
         BTPDQ_INSERT_TAIL(&m_torrents, tp, entry);
         m_ntorrents++;
         cm_start(tp);
+        tl->tp = tp;
+        free(mi);
+        return IPC_OK;
     } else {
-        clear_metainfo(&tp->meta);
+        mi_free_files(tp->nfiles, tp->files);
         free(tp);
+        free(mi);
+        return IPC_EBADTRACKER;
     }
-    return error;
 }
 
 static void
 torrent_kill(struct torrent *tp)
 {
-    btpd_log(BTPD_L_BTPD, "Removed torrent '%s'.\n", torrent_name(tp));
+    btpd_log(BTPD_L_BTPD, "Stopped torrent '%s'.\n", torrent_name(tp));
     assert(m_ntorrents > 0);
     assert(!(tr_active(tp) || net_active(tp) || cm_active(tp)));
     m_ntorrents--;
     BTPDQ_REMOVE(&m_torrents, tp, entry);
-    clear_metainfo(&tp->meta);
     tr_kill(tp);
     net_kill(tp);
     cm_kill(tp);
+    tp->tl->tp = NULL;
+    mi_free_files(tp->nfiles, tp->files);
     free(tp);
     if (m_ntorrents == 0)
         btpd_on_no_torrents();
diff --git a/btpd/torrent.h b/btpd/torrent.h
index 2b1cf19..9181109 100644
--- a/btpd/torrent.h
+++ b/btpd/torrent.h
@@ -2,7 +2,7 @@
 #define BTPD_TORRENT_H
 
 #define PIECE_BLOCKLEN (1 << 14)
-#define RELPATH_SIZE 41
+#define RELPATH_SIZE SHAHEXSIZE
 
 enum torrent_state {
     T_STARTING,
@@ -11,15 +11,22 @@ enum torrent_state {
 };
 
 struct torrent {
-    char relpath[RELPATH_SIZE];
-    struct metainfo meta;
+    struct tlib *tl;
 
+    char relpath[RELPATH_SIZE];
     enum torrent_state state;
 
     struct content *cm;
     struct tracker *tr;
     struct net *net;
 
+    off_t total_length;
+    off_t piece_length;
+    uint32_t npieces;
+    unsigned nfiles;
+    struct mi_file *files;
+    size_t pieces_off;
+
     BTPDQ_ENTRY(torrent) entry;
 };
 
@@ -27,12 +34,11 @@ BTPDQ_HEAD(torrent_tq, torrent);
 
 unsigned torrent_count(void);
 const struct torrent_tq *torrent_get_all(void);
-struct torrent *torrent_get(const uint8_t *hash);
+struct torrent *torrent_by_num(unsigned num);
+struct torrent *torrent_by_hash(const uint8_t *hash);
 
-int torrent_start(const uint8_t *hash);
+enum ipc_err torrent_start(struct tlib *tl);
 void torrent_stop(struct torrent *tp);
-int torrent_set_links(const uint8_t *hash, const char *torrent,
-    const char *content);
 
 off_t torrent_piece_size(struct torrent *tp, uint32_t piece);
 uint32_t torrent_piece_blocks(struct torrent *tp, uint32_t piece);
diff --git a/btpd/tracker_req.c b/btpd/tracker_req.c
index e172a84..c21bd5b 100644
--- a/btpd/tracker_req.c
+++ b/btpd/tracker_req.c
@@ -27,6 +27,7 @@ enum timer_type {
 };
 
 struct tracker {
+    struct mi_announce *ann;
     enum timer_type ttype;
     enum tr_event event;
     int interval;
@@ -189,7 +190,7 @@ tr_send(struct torrent *tp, enum tr_event event)
     if (tr->ttype == TIMER_TIMEOUT)
         http_cancel(tr->req);
 
-    if ((busy_secs = http_server_busy_time(tp->meta.announce, 3)) > 0) {
+    if ((busy_secs = http_server_busy_time(tr->ann->tiers[0].urls[0], 3)) > 0) {
         tr->ttype = TIMER_RETRY;
         btpd_ev_add(&tr->timer, (& (struct timeval) { busy_secs, 0 }));
         return;
@@ -198,32 +199,37 @@ tr_send(struct torrent *tp, enum tr_event event)
     tr->ttype = TIMER_TIMEOUT;
     btpd_ev_add(&tr->timer, REQ_TIMEOUT);
 
-    qc = (strchr(tp->meta.announce, '?') == NULL) ? '?' : '&';
+    qc = (strchr(tr->ann->tiers[0].urls[0], '?') == NULL) ? '?' : '&';
 
     for (int i = 0; i < 20; i++)
-        snprintf(e_hash + i * 3, 4, "%%%.2x", tp->meta.info_hash[i]);
+        snprintf(e_hash + i * 3, 4, "%%%.2x", tp->tl->hash[i]);
     for (int i = 0; i < 20; i++)
         snprintf(e_id + i * 3, 4, "%%%.2x", peer_id[i]);
 
     http_get(&tr->req, http_cb, tp,
         "%s%cinfo_hash=%s&peer_id=%s&port=%d&uploaded=%llu"
         "&downloaded=%llu&left=%llu&compact=1%s%s",
-        tp->meta.announce, qc, e_hash, e_id, net_port,
+        tr->ann->tiers[0].urls[0], qc, e_hash, e_id, net_port,
         tp->net->uploaded, tp->net->downloaded,
-        (long long)tp->meta.total_length - cm_content(tp),
+        (long long)tp->total_length - cm_content(tp),
         event == TR_EV_EMPTY ? "" : "&event=", m_events[event]);
 }
 
 int
-tr_create(struct torrent *tp)
+tr_create(struct torrent *tp, const char *mi)
 {
-    if (strncmp(tp->meta.announce, "http://", sizeof("http://") - 1) != 0) {
+    struct mi_announce *ann = mi_announce(mi);
+    if (ann == NULL)
+        btpd_err("out of memory.\n");
+    if (strncmp(ann->tiers[0].urls[0],
+            "http://", sizeof("http://") - 1) != 0) {
         btpd_log(BTPD_L_ERROR,
             "btpd currently has no support for the protocol specified in "
-            "'%s'.\n", tp->meta.announce);
+            "'%s'.\n", ann->tiers[0].urls[0]);
         return EINVAL;
     }
     tp->tr = btpd_calloc(1, sizeof(*tp->tr));
+    tp->tr->ann = ann;
     evtimer_set(&tp->tr->timer, timer_cb, tp);
     return 0;
 }
@@ -236,6 +242,7 @@ tr_kill(struct torrent *tp)
     btpd_ev_del(&tr->timer);
     if (tr->req != NULL)
         http_cancel(tr->req);
+    mi_free_announce(tr->ann);
     free(tr);
 }
 
diff --git a/btpd/tracker_req.h b/btpd/tracker_req.h
index 1e85163..23cbeaa 100644
--- a/btpd/tracker_req.h
+++ b/btpd/tracker_req.h
@@ -1,7 +1,7 @@
 #ifndef TRACKER_REQ_H
 #define TRACKER_REQ_H
 
-int tr_create(struct torrent *tp);
+int tr_create(struct torrent *tp, const char *mi);
 void tr_kill(struct torrent *tp);
 void tr_start(struct torrent *tp);
 void tr_stop(struct torrent *tp);
diff --git a/cli/Makefile.am b/cli/Makefile.am
index 84e18f9..3222e95 100644
--- a/cli/Makefile.am
+++ b/cli/Makefile.am
@@ -5,7 +5,7 @@ btinfo_LDADD=../misc/libmisc.a -lcrypto -lm
 btinfo_CPPFLAGS=-I$(top_srcdir)/misc @openssl_CPPFLAGS@
 btinfo_LDFLAGS=@openssl_LDFLAGS@
 
-btcli_SOURCES=btcli.c btpd_if.c btpd_if.h
+btcli_SOURCES=btcli.c btcli.h add.c del.c list.c kill.c start.c stop.c stat.c
 btcli_LDADD=../misc/libmisc.a -lcrypto -lm
 btcli_CPPFLAGS=-I$(top_srcdir)/misc @openssl_CPPFLAGS@
 btcli_LDFLAGS=@openssl_LDFLAGS@
diff --git a/cli/add.c b/cli/add.c
new file mode 100644
index 0000000..34edb1c
--- /dev/null
+++ b/cli/add.c
@@ -0,0 +1,87 @@
+#include "btcli.h"
+
+void
+usage_add(void)
+{
+    printf(
+        "Add torrents to btpd.\n"
+        "\n"
+        "Usage: add [--topdir] -d dir file\n"
+        "       add file ...\n"
+        "\n"
+        "Arguments:\n"
+        "file ...\n"
+        "\tOne or more torrents to add.\n"
+        "\n"
+        "Options:\n"
+        "-d dir\n"
+        "\tUse the dir for content.\n"
+        "\n"
+        "--topdir\n"
+        "\tAppend the torrent top directory (if any) to the content path.\n"
+        "\tThis option cannot be used without the '-d' option.\n"
+        "\n"
+        );
+    exit(1);
+}
+
+static struct option add_opts [] = {
+    { "help", no_argument, NULL, 'H' },
+    { "topdir", no_argument, NULL, 'T'},
+    {NULL, 0, NULL, 0}
+};
+
+void
+cmd_add(int argc, char **argv)
+{
+    int ch, topdir = 0;
+    size_t dirlen = 0;
+    char *dir = NULL, *name = NULL;
+
+    while ((ch = getopt_long(argc, argv, "d:n:", add_opts, NULL)) != -1) {
+        switch (ch) {
+        case 'T':
+            topdir = 1;
+            break;
+        case 'd':
+            dir = optarg;
+            if ((dirlen = strlen(dir)) == 0)
+                errx(1, "bad option value for -d");
+            break;
+        case 'n':
+            name = optarg;
+            break;
+        default:
+            usage_add();
+        }
+    }
+    argc -= optind;
+    argv += optind;
+
+    if (argc != 1 || dir == NULL)
+        usage_add();
+
+    btpd_connect();
+    char *mi;
+    size_t mi_size;
+    char dpath[PATH_MAX];
+    struct io_buffer iob;
+
+    if ((mi = mi_load(argv[0], &mi_size)) == NULL)
+        err(1, "error loading '%s'", argv[0]);
+
+    buf_init(&iob, PATH_MAX);
+    buf_write(&iob, dir, dirlen);
+    if (topdir) {
+        size_t tdlen;
+        const char *td =
+            benc_dget_mem(benc_dget_dct(mi, "info"), "name", &tdlen);
+        buf_swrite(&iob, "/");
+        buf_write(&iob, td, tdlen);
+    }
+    buf_swrite(&iob, "");
+    if (realpath(iob.buf, dpath) == NULL)
+        err(1, "realpath '%s'", dpath);
+    handle_ipc_res(btpd_add(ipc, mi, mi_size, dpath, name), argv[0]);
+    return;
+}
diff --git a/cli/btcli.c b/cli/btcli.c
index 95ae487..f8b6a81 100644
--- a/cli/btcli.c
+++ b/cli/btcli.c
@@ -1,21 +1,4 @@
-#include <sys/types.h>
-#include <sys/stat.h>
-
-#include <err.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <getopt.h>
-#include <inttypes.h>
-#include <limits.h>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "btpd_if.h"
-#include "metainfo.h"
-#include "subr.h"
+#include "btcli.h"
 
 const char *btpd_dir;
 struct ipc *ipc;
@@ -27,377 +10,55 @@ btpd_connect(void)
         err(1, "cannot open connection to btpd in %s", btpd_dir);
 }
 
-enum ipc_code
-handle_ipc_res(enum ipc_code code, const char *target)
+enum ipc_err
+handle_ipc_res(enum ipc_err code, const char *target)
 {
     switch (code) {
     case IPC_OK:
         break;
-    case IPC_FAIL:
-        warnx("btpd couldn't execute the requested operation for %s", target);
-        break;
-    case IPC_ERROR:
-        warnx("btpd encountered an error for %s", target);
-        break;
-    default:
+    case IPC_COMMERR:
         errx(1, "fatal error in communication with btpd");
+    default:
+        warnx("btpd response for '%s': %s", target, ipc_strerror(code));
     }
     return code;
 }
 
 char
-state_char(struct tpstat *ts)
+tstate_char(enum ipc_tstate ts)
 {
-    switch (ts->state) {
-    case T_STARTING:
+    switch (ts) {
+    case IPC_TSTATE_INACTIVE:
+        return 'I';
+    case IPC_TSTATE_START:
         return '+';
-    case T_ACTIVE:
-        return ts->pieces_got == ts->torrent_pieces ? 'S' : 'L';
-    case T_STOPPING:
+    case IPC_TSTATE_STOP:
         return '-';
-    default:
-        return ' ';
+    case IPC_TSTATE_LEECH:
+        return 'L';
+    case IPC_TSTATE_SEED:
+        return 'S';
     }
+    errx(1, "bad state");
 }
 
-void
-print_stat(struct tpstat *ts)
-{
-    printf("%c %5.1f%% %6.1fM %7.2fkB/s %6.1fM %7.2fkB/s %4u %5.1f%%",
-        state_char(ts),
-        floor(1000.0 * ts->content_got / ts->content_size) / 10,
-        (double)ts->downloaded / (1 << 20),
-        (double)ts->rate_down / (20 << 10),
-        (double)ts->uploaded / (1 << 20),
-        (double)ts->rate_up / (20 << 10),
-        ts->peers,
-        floor(1000.0 * ts->pieces_seen / ts->torrent_pieces) / 10);
-    if (ts->tr_errors > 0)
-        printf(" E%u", ts->tr_errors);
-    printf("\n");
-}
-
-void
-usage_add(void)
-{
-    printf(
-        "Add torrents to btpd.\n"
-        "\n"
-        "Usage: add [--topdir] -d dir file\n"
-        "       add file ...\n"
-        "\n"
-        "Arguments:\n"
-        "file ...\n"
-        "\tOne or more torrents to add.\n"
-        "\n"
-        "Options:\n"
-        "-d dir\n"
-        "\tUse the dir for content.\n"
-        "\n"
-        "--topdir\n"
-        "\tAppend the torrent top directory (if any) to the content path.\n"
-        "\tThis option cannot be used without the '-d' option.\n"
-        "\n"
-        );
-    exit(1);
-}
-
-struct option add_opts [] = {
-    { "help", no_argument, NULL, 'H' },
-    { "topdir", no_argument, NULL, 'T'},
-    {NULL, 0, NULL, 0}
-};
-
 int
-content_link(uint8_t *hash, char *buf)
-{
-    int n;
-    char relpath[41];
-    char path[PATH_MAX];
-    for (int i = 0; i < 20; i++)
-        snprintf(relpath + i * 2, 3, "%.2x", hash[i]);
-    snprintf(path, PATH_MAX, "%s/torrents/%s/content", btpd_dir, relpath);
-    if ((n = readlink(path, buf, PATH_MAX)) == -1)
-        return errno;
-    buf[min(n, PATH_MAX)] = '\0';
-    return 0;
-}
-
-void
-cmd_add(int argc, char **argv)
-{
-    int ch, topdir = 0;
-    char *dir = NULL;
-
-    while ((ch = getopt_long(argc, argv, "d:", add_opts, NULL)) != -1) {
-        switch (ch) {
-        case 'T':
-            topdir = 1;
-            break;
-        case 'd':
-            dir = optarg;
-            break;
-        default:
-            usage_add();
-        }
-    }
-    argc -= optind;
-    argv += optind;
-
-    if (argc < 1 || (topdir == 1 && dir == NULL) || (dir != NULL && argc > 1))
-        usage_add();
-
-    btpd_connect();
-    for (int i = 0; i < argc; i++) {
-        struct metainfo *mi;
-        char rdpath[PATH_MAX], dpath[PATH_MAX], fpath[PATH_MAX];
-
-        if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0) {
-            warn("error loading torrent %s", argv[i]);
-            continue;
-        }
-
-        if ((topdir &&
-                !(mi->nfiles == 1
-                    && strcmp(mi->name, mi->files[0].path) == 0)))
-            snprintf(dpath, PATH_MAX, "%s/%s", dir, mi->name);
-        else if (dir != NULL)
-            strncpy(dpath, dir, PATH_MAX);
-        else {
-            if (content_link(mi->info_hash, dpath) != 0) {
-                warnx("unknown content dir for %s", argv[i]);
-                errx(1, "use the '-d' option");
-            }
-        }
-
-        if (mkdir(dpath, 0777) != 0 && errno != EEXIST)
-            err(1, "couldn't create directory %s", dpath);
-
-        if (realpath(dpath, rdpath) == NULL)
-            err(1, "path error on %s", dpath);
-
-        if (realpath(argv[i], fpath) == NULL)
-            err(1, "path error on %s", fpath);
-
-        handle_ipc_res(btpd_add(ipc, mi->info_hash, fpath, rdpath), argv[i]);
-        clear_metainfo(mi);
-        free(mi);
-    }
-}
-
-void
-usage_del(void)
-{
-    printf(
-        "Remove torrents from btpd.\n"
-        "\n"
-        "Usage: del file ...\n"
-        "\n"
-        "Arguments:\n"
-        "file ...\n"
-        "\tThe torrents to remove.\n"
-        "\n");
-    exit(1);
-}
-
-void
-cmd_del(int argc, char **argv)
-{
-    if (argc < 2)
-        usage_del();
-
-    btpd_connect();
-    for (int i = 1; i < argc; i++) {
-        struct metainfo *mi;
-        if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0) {
-            warn("error loading torrent %s", argv[i]);
-            continue;
-        }
-        handle_ipc_res(btpd_del(ipc, mi->info_hash), argv[i]);
-        clear_metainfo(mi);
-        free(mi);
-    }
-}
-
-void
-usage_kill(void)
-{
-    printf(
-        "Shutdown btpd.\n"
-        "\n"
-        "Usage: kill [seconds]\n"
-        "\n"
-        "Arguments:\n"
-        "seconds\n"
-        "\tThe number of seconds btpd waits before giving up on unresponsive\n"
-        "\ttrackers.\n"
-        "\n"
-        );
-    exit(1);
-}
-
-void
-cmd_kill(int argc, char **argv)
-{
-    int seconds = -1;
-    char *endptr;
-
-    if (argc == 2) {
-        seconds = strtol(argv[1], &endptr, 10);
-        if (strlen(argv[1]) > endptr - argv[1] || seconds < 0)
-            usage_kill();
-    } else if (argc > 2)
-        usage_kill();
-
-    btpd_connect();
-    handle_ipc_res(btpd_die(ipc, seconds), "kill");
-}
-
-void
-usage_list(void)
-{
-    printf(
-        "List active torrents.\n"
-        "\n"
-        "Usage: list\n"
-        "\n"
-        );
-    exit(1);
-}
-
-void
-cmd_list(int argc, char **argv)
-{
-    struct btstat *st;
-
-    if (argc > 1)
-        usage_list();
-
-    btpd_connect();
-    if (handle_ipc_res(btpd_stat(ipc, &st), "list") != IPC_OK)
-        exit(1);
-    for (int i = 0; i < st->ntorrents; i++) {
-        struct tpstat *ts = &st->torrents[i];
-        printf("%c. %s\n", state_char(ts), ts->name);
-    }
-    printf("%u torrent%s.\n", st->ntorrents,
-        st->ntorrents == 1 ? "" : "s");
-}
-
-void
-usage_stat(void)
-{
-    printf(
-        "Display stats for active torrents.\n"
-        "The displayed stats are:\n"
-        "%% got, MB down, rate down. MB up, rate up\n"
-        "peer count, %% of pieces seen, tracker errors\n"
-        "\n"
-        "Usage: stat [-i] [-w seconds] [file ...]\n"
-        "\n"
-        "Arguments:\n"
-        "file ...\n"
-        "\tOnly display stats for the given torrent(s).\n"
-        "\n"
-        "Options:\n"
-        "-i\n"
-        "\tDisplay individual lines for each torrent.\n"
-        "\n"
-        "-w n\n"
-        "\tDisplay stats every n seconds.\n"
-        "\n");
-    exit(1);
-}
-
-void
-do_stat(int individual, int seconds, int hash_count, uint8_t (*hashes)[20])
-{
-    struct btstat *st;
-    struct tpstat tot;
-again:
-    bzero(&tot, sizeof(tot));
-    tot.state = -1;
-    if (handle_ipc_res(btpd_stat(ipc, &st), "stat") != IPC_OK)
-        exit(1);
-    for (int i = 0; i < st->ntorrents; i++) {
-        struct tpstat *cur = &st->torrents[i];
-        if (hash_count > 0) {
-            int found = 0;
-            for (int h = 0; !found && h < hash_count; h++)
-                if (bcmp(cur->hash, hashes[h], 20) == 0)
-                    found = 1;
-            if (!found)
-                continue;
-        }
-        tot.uploaded += cur->uploaded;
-        tot.downloaded += cur->downloaded;
-        tot.rate_up += cur->rate_up;
-        tot.rate_down += cur->rate_down;
-        tot.peers += cur->peers;
-        tot.pieces_seen += cur->pieces_seen;
-        tot.torrent_pieces += cur->torrent_pieces;
-        tot.content_got += cur->content_got;
-        tot.content_size += cur->content_size;
-        if (cur->tr_errors > 0)
-            tot.tr_errors++;
-        if (individual) {
-            printf("%s:\n", cur->name);
-            print_stat(cur);
-        }
-    }
-    free_btstat(st);
-    if (individual)
-        printf("Total:\n");
-    print_stat(&tot);
-    if (seconds > 0) {
-        sleep(seconds);
-        goto again;
-    }
-}
-
-struct option stat_opts [] = {
-    { "help", no_argument, NULL, 'H' },
-    {NULL, 0, NULL, 0}
-};
-
-void
-cmd_stat(int argc, char **argv)
+torrent_spec(char *arg, struct ipc_torrent *tp)
 {
-    int ch;
-    int wflag = 0, iflag = 0, seconds = 0;
-    uint8_t (*hashes)[20] = NULL;
-    char *endptr;
-    while ((ch = getopt_long(argc, argv, "iw:", stat_opts, NULL)) != -1) {
-        switch (ch) {
-        case 'i':
-            iflag = 1;
-            break;
-        case 'w':
-            wflag = 1;
-            seconds = strtol(optarg, &endptr, 10);
-            if (strlen(optarg) > endptr - optarg || seconds < 1)
-                usage_stat();
-            break;
-        default:
-            usage_stat();
-        }
+    char *p;
+    tp->u.num = strtoul(arg, &p, 10);
+    if (*p == '\0') {
+        tp->by_hash = 0;
+        return 1;
     }
-    argc -= optind;
-    argv += optind;
-
-    if (argc > 0) {
-        hashes = malloc(argc * 20);
-        for (int i = 0; i < argc; i++) {
-            struct metainfo *mi;
-            if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0)
-                err(1, "error loading torrent %s", argv[i]);
-            bcopy(mi->info_hash, hashes[i], 20);
-            clear_metainfo(mi);
-            free(mi);
-        }
+    if ((p = mi_load(arg, NULL)) == NULL) {
+        warnx("bad torrent '%s' (%s)", arg, strerror(errno));
+        return 0;
     }
-    btpd_connect();
-    do_stat(iflag, seconds, argc, hashes);
+    tp->by_hash = 1;
+    mi_info_hash(p, tp->u.hash);
+    free(p);
+    return 1;
 }
 
 struct {
@@ -409,6 +70,8 @@ struct {
     { "del", cmd_del, usage_del },
     { "kill", cmd_kill, usage_kill },
     { "list", cmd_list, usage_list },
+    { "start", cmd_start, usage_start },
+    { "stop", cmd_stop, usage_stop },
     { "stat", cmd_stat, usage_stat }
 };
 
@@ -434,7 +97,9 @@ usage(void)
         "del\n"
         "kill\n"
         "list\n"
+        "start\n"
         "stat\n"
+        "stop\n"
         "\n");
     exit(1);
 }
diff --git a/cli/btcli.h b/cli/btcli.h
new file mode 100644
index 0000000..5b64662
--- /dev/null
+++ b/cli/btcli.h
@@ -0,0 +1,44 @@
+#ifndef BTCLI_H
+#define BTCLI_H
+
+#include <err.h>
+#include <errno.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "btpd_if.h"
+#include "metainfo.h"
+#include "subr.h"
+#include "benc.h"
+#include "iobuf.h"
+#include "queue.h"
+
+extern const char *btpd_dir;
+extern struct ipc *ipc;
+
+void btpd_connect(void);
+enum ipc_err handle_ipc_res(enum ipc_err err, const char *target);
+
+char tstate_char(enum ipc_tstate ts);
+int torrent_spec(char *arg, struct ipc_torrent *tp);
+
+void usage_add(void);
+void cmd_add(int argc, char **argv);
+void usage_del(void);
+void cmd_del(int argc, char **argv);
+void usage_list(void);
+void cmd_list(int argc, char **argv);
+void usage_stat(void);
+void cmd_stat(int argc, char **argv);
+void usage_kill(void);
+void cmd_kill(int argc, char **argv);
+void usage_start(void);
+void cmd_start(int argc, char **argv);
+void usage_stop(void);
+void cmd_stop(int argc, char **argv);
+
+#endif
diff --git a/cli/btinfo.c b/cli/btinfo.c
index f451c85..cfd1991 100644
--- a/cli/btinfo.c
+++ b/cli/btinfo.c
@@ -8,6 +8,7 @@
 #include <stdlib.h>
 
 #include "metainfo.h"
+#include "subr.h"
 
 static void
 usage()
@@ -21,11 +22,38 @@ static struct option longopts[] = {
     { NULL, 0, NULL, 0 }
 };
 
+static void
+print_metainfo(const char *mi)
+{
+    uint8_t hash[20];
+    char hex[SHAHEXSIZE];
+    char *name = mi_name(mi);
+    unsigned nfiles = mi_nfiles(mi);
+    struct mi_file *files = mi_files(mi);
+    struct mi_announce *ann = mi_announce(mi);
+    for (int i = 0; i < ann->ntiers; i++)
+        for (int j = 0; j < ann->tiers[i].nurls; j++)
+            printf("%d: %s\n", i, ann->tiers[i].urls[j]);
+    printf("\n");
+    mi_free_announce(ann);
+    mi_info_hash(mi, hash);
+    bin2hex(hash, hex, 20);
+    printf("name: %s\n", name);
+    printf("info hash: %s\n", hex);
+    printf("length: %jd\n", (intmax_t)mi_total_length(mi));
+    printf("piece length: %jd\n", (intmax_t)mi_piece_length(mi));
+    printf("files: %u\n", nfiles);
+    for (unsigned i = 0; i < nfiles; i++)
+        printf("%s(%jd)\n", files[i].path, (intmax_t)files[i].length);
+    free(name);
+}
+
 int
 main(int argc, char **argv)
 {
     int ch;
 
+    srandom(time(NULL));
     while ((ch = getopt_long(argc, argv, "", longopts, NULL)) != -1)
         usage();
 
@@ -36,13 +64,12 @@ main(int argc, char **argv)
         usage();
 
     while (argc > 0) {
-        struct metainfo *mi;
+        char *mi = NULL;
 
-        if ((errno = load_metainfo(*argv, -1, 1, &mi)) != 0)
-            err(1, "load_metainfo: %s", *argv);
+        if ((mi = mi_load(*argv, NULL)) == NULL)
+            err(1, "mi_load: %s", *argv);
 
         print_metainfo(mi);
-        clear_metainfo(mi);
         free(mi);
 
         argc--;
diff --git a/cli/del.c b/cli/del.c
new file mode 100644
index 0000000..eaa0a1e
--- /dev/null
+++ b/cli/del.c
@@ -0,0 +1,30 @@
+#include "btcli.h"
+
+void
+usage_del(void)
+{
+    printf(
+        "Remove torrents from btpd.\n"
+        "\n"
+        "Usage: del torrent ...\n"
+        "\n"
+        "Arguments:\n"
+        "file ...\n"
+        "\tThe torrents to remove.\n"
+        "\n");
+    exit(1);
+}
+
+void
+cmd_del(int argc, char **argv)
+{
+    struct ipc_torrent t;
+
+    if (argc < 2)
+        usage_del();
+
+    btpd_connect();
+    for (int i = 1; i < argc; i++)
+        if (torrent_spec(argv[i], &t))
+            handle_ipc_res(btpd_del(ipc, &t), argv[i]);
+}
diff --git a/cli/kill.c b/cli/kill.c
new file mode 100644
index 0000000..b2c6862
--- /dev/null
+++ b/cli/kill.c
@@ -0,0 +1,35 @@
+#include "btcli.h"
+
+void
+usage_kill(void)
+{
+    printf(
+        "Shutdown btpd.\n"
+        "\n"
+        "Usage: kill [seconds]\n"
+        "\n"
+        "Arguments:\n"
+        "seconds\n"
+        "\tThe number of seconds btpd waits before giving up on unresponsive\n"
+        "\ttrackers.\n"
+        "\n"
+        );
+    exit(1);
+}
+
+void
+cmd_kill(int argc, char **argv)
+{
+    int seconds = -1;
+    char *endptr;
+
+    if (argc == 2) {
+        seconds = strtol(argv[1], &endptr, 10);
+        if (strlen(argv[1]) > endptr - argv[1] || seconds < 0)
+            usage_kill();
+    } else if (argc > 2)
+        usage_kill();
+
+    btpd_connect();
+    handle_ipc_res(btpd_die(ipc, seconds), "kill");
+}
diff --git a/cli/list.c b/cli/list.c
new file mode 100644
index 0000000..8bdf851
--- /dev/null
+++ b/cli/list.c
@@ -0,0 +1,124 @@
+#include "btcli.h"
+
+void
+usage_list(void)
+{
+    printf(
+        "List torrents.\n"
+        "\n"
+        "Usage: list [-a] [-i]\n"
+        "\n"
+        );
+    exit(1);
+}
+
+struct item {
+    unsigned num;
+    char *name;
+    char st;
+    BTPDQ_ENTRY(item) entry;
+};
+
+struct items {
+    int count;
+    BTPDQ_HEAD(item_tq, item) hd;
+};
+
+void
+itm_insert(struct items *itms, struct item *itm)
+{
+    struct item *p;
+    BTPDQ_FOREACH(p, &itms->hd, entry)
+        if (itm->num < p->num)
+#if 0
+        if (strcmp(itm->name, p->name) < 0)
+#endif
+            break;
+    if (p != NULL)
+        BTPDQ_INSERT_BEFORE(p, itm, entry);
+    else
+        BTPDQ_INSERT_TAIL(&itms->hd, itm, entry);
+}
+
+static void
+list_cb(int obji, enum ipc_err objerr, struct ipc_get_res *res, void *arg)
+{
+    struct items *itms = arg;
+    struct item *itm = calloc(1, sizeof(*itm));
+    itms->count++;
+    itm->num = (unsigned)res[IPC_TVAL_NUM].v.num;
+    itm->st = tstate_char(res[IPC_TVAL_STATE].v.num);
+    if (res[IPC_TVAL_NAME].type == IPC_TYPE_ERR)
+        asprintf(&itm->name, "%s", ipc_strerror(res[IPC_TVAL_NAME].v.num));
+    else
+        asprintf(&itm->name, "%.*s", (int)res[IPC_TVAL_NAME].v.str.l,
+            res[IPC_TVAL_NAME].v.str.p);
+    itm_insert(itms, itm);
+#if 0
+    int *count = arg;
+    (*count)++;
+    printf("%4u %c.", (unsigned)res[IPC_TVAL_NUM].v.num,
+        tstate_char(res[IPC_TVAL_STATE].v.num));
+    if (res[IPC_TVAL_NAME].type == IPC_TYPE_ERR)
+        printf(" %s\n", ipc_strerror(res[IPC_TVAL_NAME].v.num));
+    else
+        printf(" %.*s\n", (int)res[IPC_TVAL_NAME].v.str.l,
+            res[IPC_TVAL_NAME].v.str.p);
+#endif
+}
+
+void
+print_items(struct items* itms)
+{
+    int n;
+    struct item *p;
+    BTPDQ_FOREACH(p, &itms->hd, entry) {
+        n = printf("%u: ", p->num);
+        while (n < 7) {
+            putchar(' ');
+            n++;
+        }
+        printf("%c. %s\n", p->st, p->name);
+    }
+}
+
+static struct option list_opts [] = {
+    { "help", no_argument, NULL, 'H' },
+    {NULL, 0, NULL, 0}
+};
+
+void
+cmd_list(int argc, char **argv)
+{
+    int ch, /*count = 0,*/ inactive = 0, active = 0;
+    enum ipc_twc twc;
+    enum ipc_tval keys[] = { IPC_TVAL_NUM, IPC_TVAL_STATE, IPC_TVAL_NAME };
+    struct items itms;
+    while ((ch = getopt_long(argc, argv, "ai", list_opts, NULL)) != -1) {
+        switch (ch) {
+        case 'a':
+            active = 1;
+            break;
+        case 'i':
+            inactive = 1;
+            break;
+        default:
+            usage_list();
+        }
+    }
+
+    if (inactive == active)
+        twc = IPC_TWC_ALL;
+    else if (inactive)
+        twc = IPC_TWC_INACTIVE;
+    else
+        twc = IPC_TWC_ACTIVE;
+
+    btpd_connect();
+    printf("NUM    ST NAME\n");
+    itms.count = 0;
+    BTPDQ_INIT(&itms.hd);
+    handle_ipc_res(btpd_tget_wc(ipc, twc, keys, 3, list_cb, &itms), "tget");
+    print_items(&itms);
+    printf("Listed %d torrent%s.\n", itms.count, itms.count == 1 ? "" : "s");
+}
diff --git a/cli/start.c b/cli/start.c
new file mode 100644
index 0000000..4772bb6
--- /dev/null
+++ b/cli/start.c
@@ -0,0 +1,27 @@
+#include "btcli.h"
+
+void
+usage_start(void)
+{
+    printf(
+        "Start torrents.\n"
+        "\n"
+        "Usage: start torrent\n"
+        "\n"
+        );
+    exit(1);
+}
+
+void
+cmd_start(int argc, char **argv)
+{
+    struct ipc_torrent t;
+
+    if (argc < 2)
+        usage_start();
+
+    btpd_connect();
+    for (int i = 1; i < argc; i++)
+        if (torrent_spec(argv[i], &t))
+            handle_ipc_res(btpd_start(ipc, &t), argv[i]);
+}
diff --git a/cli/stat.c b/cli/stat.c
new file mode 100644
index 0000000..2af1210
--- /dev/null
+++ b/cli/stat.c
@@ -0,0 +1,193 @@
+#include <math.h>
+
+#include "btcli.h"
+
+void
+usage_stat(void)
+{
+    printf(
+        "Display stats for active torrents.\n"
+        "\n"
+        "Usage: stat [-i] [-w seconds] [file ...]\n"
+        "\n"
+        "Arguments:\n"
+        "file ...\n"
+        "\tOnly display stats for the given torrent(s).\n"
+        "\n"
+        "Options:\n"
+        "-i\n"
+        "\tDisplay individual lines for each torrent.\n"
+        "\n"
+        "-n\n"
+        "\tDisplay the name of each torrent. Implies '-i'.\n"
+        "\n"
+        "-w n\n"
+        "\tDisplay stats every n seconds.\n"
+        "\n");
+    exit(1);
+}
+
+struct btstat {
+    unsigned num;
+    enum ipc_tstate state;
+    unsigned peers, tr_errors;
+    long long content_got, content_size, downloaded, uploaded, rate_up,
+        rate_down;
+    uint32_t pieces_seen, torrent_pieces;
+};
+
+struct cbarg {
+    int individual, names;
+    struct btstat tot;
+};
+
+static enum ipc_tval stkeys[] = {
+    IPC_TVAL_STATE,
+    IPC_TVAL_NUM,
+    IPC_TVAL_NAME,
+    IPC_TVAL_PCOUNT,
+    IPC_TVAL_TRERR,
+    IPC_TVAL_PCCOUNT,
+    IPC_TVAL_PCSEEN,
+    IPC_TVAL_SESSUP,
+    IPC_TVAL_SESSDWN,
+    IPC_TVAL_RATEUP,
+    IPC_TVAL_RATEDWN,
+    IPC_TVAL_CGOT,
+    IPC_TVAL_CSIZE
+};
+
+static size_t nstkeys = sizeof(stkeys) / sizeof(stkeys[0]);
+
+static void
+print_stat(struct btstat *st)
+{
+    printf("%5.1f%% %6.1fM %7.2fkB/s %6.1fM %7.2fkB/s %5u %5.1f%%",
+        floor(1000.0 * st->content_got / st->content_size) / 10,
+        (double)st->downloaded / (1 << 20),
+        (double)st->rate_down / (20 << 10),
+        (double)st->uploaded / (1 << 20),
+        (double)st->rate_up / (20 << 10),
+        st->peers,
+        floor(1000.0 * st->pieces_seen / st->torrent_pieces) / 10);
+    if (st->tr_errors > 0)
+        printf(" E%u", st->tr_errors);
+    printf("\n");
+}
+
+void
+stat_cb(int obji, enum ipc_err objerr, struct ipc_get_res *res, void *arg)
+{
+    struct cbarg *cba = arg;
+    struct btstat st, *tot = &cba->tot;
+    if (objerr != IPC_OK || res[IPC_TVAL_STATE].v.num == IPC_TSTATE_INACTIVE)
+        return;
+    bzero(&st, sizeof(st));
+    st.state = res[IPC_TVAL_STATE].v.num;
+    st.num = res[IPC_TVAL_NUM].v.num;
+    tot->torrent_pieces += (st.torrent_pieces = res[IPC_TVAL_PCCOUNT].v.num);
+    tot->pieces_seen += (st.pieces_seen = res[IPC_TVAL_PCSEEN].v.num);
+    tot->content_got += (st.content_got = res[IPC_TVAL_CGOT].v.num);
+    tot->content_size += (st.content_size = res[IPC_TVAL_CSIZE].v.num);
+    tot->downloaded += (st.downloaded = res[IPC_TVAL_SESSDWN].v.num);
+    tot->uploaded += (st.uploaded = res[IPC_TVAL_SESSUP].v.num);
+    tot->rate_down += (st.rate_down = res[IPC_TVAL_RATEDWN].v.num);
+    tot->rate_up += (st.rate_up = res[IPC_TVAL_RATEUP].v.num);
+    tot->peers += (st.peers = res[IPC_TVAL_PCOUNT].v.num);
+    if ((st.tr_errors = res[IPC_TVAL_TRERR].v.num) > 0)
+        tot->tr_errors++;
+    if (cba->individual) {
+        if (cba->names)
+            printf("%.*s\n", (int)res[IPC_TVAL_NAME].v.str.l,
+                res[IPC_TVAL_NAME].v.str.p);
+        int n = printf("%u:", st.num);
+        while (n < 7) {
+            putchar(' ');
+            n++;
+        }
+        printf("%c. ", tstate_char(st.state));
+        print_stat(&st);
+    }
+}
+
+static void
+do_stat(int individual, int names, int seconds, struct ipc_torrent *tps,
+    int ntps)
+{
+    enum ipc_err err;
+    struct cbarg cba;
+    if (names)
+        individual = 1;
+    if (individual)
+        printf("NUM    ST ");
+    printf("  HAVE   DLOAD       RTDWN   ULOAD        RTUP PEERS  AVAIL\n");
+    cba.individual = individual;
+    cba.names = names;
+again:
+    bzero(&cba.tot, sizeof(cba.tot));
+    cba.tot.state = IPC_TSTATE_INACTIVE;
+    if (tps == NULL)
+        err = btpd_tget_wc(ipc, IPC_TWC_ACTIVE, stkeys, nstkeys,
+            stat_cb, &cba);
+    else
+        err = btpd_tget(ipc, tps, ntps, stkeys, nstkeys, stat_cb, &cba);
+    if (handle_ipc_res(err, "stat") != IPC_OK)
+        exit(1);
+    if (names)
+        printf("-----\n");
+    if (individual)
+        printf("Total:    ");
+    print_stat(&cba.tot);
+    if (seconds > 0) {
+        sleep(seconds);
+        goto again;
+    }
+}
+
+static struct option stat_opts [] = {
+    { "help", no_argument, NULL, 'H' },
+    {NULL, 0, NULL, 0}
+};
+
+void
+cmd_stat(int argc, char **argv)
+{
+    int ch;
+    int wflag = 0, iflag = 0, nflag = 0, seconds = 0;
+    struct ipc_torrent *tps = NULL;
+    int ntps = 0;
+    char *endptr;
+    while ((ch = getopt_long(argc, argv, "inw:", stat_opts, NULL)) != -1) {
+        switch (ch) {
+        case 'i':
+            iflag = 1;
+            break;
+        case 'n':
+            nflag = 1;
+            break;
+        case 'w':
+            wflag = 1;
+            seconds = strtol(optarg, &endptr, 10);
+            if (*endptr != '\0' || seconds < 1)
+                usage_stat();
+            break;
+        default:
+            usage_stat();
+        }
+    }
+    argc -= optind;
+    argv += optind;
+
+    if (argc > 0) {
+        tps = malloc(argc * sizeof(*tps));
+        for (int i = 0; i < argc; i++) {
+            if (torrent_spec(argv[i], &tps[ntps]))
+                ntps++;
+            else
+                exit(1);
+
+        }
+    }
+    btpd_connect();
+    do_stat(iflag, nflag, seconds, tps, ntps);
+}
diff --git a/cli/stop.c b/cli/stop.c
new file mode 100644
index 0000000..caf68f4
--- /dev/null
+++ b/cli/stop.c
@@ -0,0 +1,27 @@
+#include "btcli.h"
+
+void
+usage_stop(void)
+{
+    printf(
+        "Stop torrents.\n"
+        "\n"
+        "Usage: stop torrent ...\n"
+        "\n"
+        );
+    exit(1);
+}
+
+void
+cmd_stop(int argc, char **argv)
+{
+    struct ipc_torrent t;
+
+    if (argc < 2)
+        usage_stop();
+
+    btpd_connect();
+    for (int i = 1; i < argc; i++)
+        if (torrent_spec(argv[i], &t))
+            handle_ipc_res(btpd_stop(ipc, &t), argv[i]);
+}