Skip to content

Instantly share code, notes, and snippets.

@jhenkens
Last active August 29, 2015 14:01
Show Gist options
  • Save jhenkens/935e4096da16c5f28990 to your computer and use it in GitHub Desktop.
Save jhenkens/935e4096da16c5f28990 to your computer and use it in GitHub Desktop.
Patch to transmission to enable sequential torrenting on a per-torrent basis in the web gui. This allows 'streaming' media as it is downloaded.
Index: daemon/remote.c
===================================================================
--- daemon/remote.c (revision 14252)
+++ daemon/remote.c (working copy)
@@ -718,6 +718,7 @@
TR_KEY_seedRatioMode,
TR_KEY_seedRatioLimit,
TR_KEY_sizeWhenDone,
+ TR_KEY_sequential,
TR_KEY_startDate,
TR_KEY_status,
TR_KEY_totalSize,
@@ -741,6 +742,7 @@
TR_KEY_rateDownload,
TR_KEY_rateUpload,
TR_KEY_sizeWhenDone,
+ TR_KEY_sequential,
TR_KEY_status,
TR_KEY_uploadRatio
};
@@ -1011,7 +1013,7 @@
if (tr_variantDictFindInt (t, TR_KEY_secondsSeeding, &i) && (i > 0))
printf (" Seeding Time: %s\n", tr_strltime (buf, i, sizeof (buf)));
printf ("\n");
-
+
printf ("ORIGINS\n");
if (tr_variantDictFindInt (t, TR_KEY_dateCreated, &i) && i)
{
@@ -1072,7 +1074,10 @@
if (tr_variantDictFindInt (t, TR_KEY_bandwidthPriority, &i))
printf (" Bandwidth Priority: %s\n",
bandwidthPriorityNames[ (i + 1) & 3]);
-
+ if (tr_variantDictFindStr (t, TR_KEY_sequential, &str, NULL) && str && *str)
+ {
+ printf (" Downloading sequentially: %s\n", str);
+ }
printf ("\n");
}
}
Index: libtransmission/peer-mgr.c
===================================================================
--- libtransmission/peer-mgr.c (revision 14252)
+++ libtransmission/peer-mgr.c (working copy)
@@ -997,17 +997,19 @@
assert (state==PIECES_SORTED_BY_INDEX
|| state==PIECES_SORTED_BY_WEIGHT);
- if (state == PIECES_SORTED_BY_WEIGHT)
+ if (!s->tor->isSequential && state == PIECES_SORTED_BY_WEIGHT)
{
setComparePieceByWeightTorrent (s);
qsort (s->pieces, s->pieceCount, sizeof (struct weighted_piece), comparePieceByWeight);
+ s->pieceSortState = PIECES_SORTED_BY_WEIGHT;
+ tr_logAddDebug("Sorted pieces by weight");
}
else
{
qsort (s->pieces, s->pieceCount, sizeof (struct weighted_piece), comparePieceByIndex);
+ tr_logAddDebug("Sorted pieces by index");
+ s->pieceSortState = PIECES_SORTED_BY_INDEX;
}
-
- s->pieceSortState = state;
}
/**
@@ -1127,7 +1129,7 @@
s->pieces = pieces;
s->pieceCount = pieceCount;
- pieceListSort (s, PIECES_SORTED_BY_WEIGHT);
+ pieceListSort (s, (s->tor->isSequential?PIECES_SORTED_BY_INDEX:PIECES_SORTED_BY_WEIGHT));
/* cleanup */
tr_free (pool);
@@ -1167,15 +1169,24 @@
/* is the torrent already sorted? */
pos = p - s->pieces;
- setComparePieceByWeightTorrent (s);
- if (isSorted && (pos > 0) && (comparePieceByWeight (p-1, p) > 0))
- isSorted = false;
- if (isSorted && (pos < s->pieceCount - 1) && (comparePieceByWeight (p, p+1) > 0))
- isSorted = false;
+ if (s->tor->isSequential){
+ if (isSorted && (pos > 0) && (comparePieceByIndex(p-1, p) > 0))
+ isSorted = false;
+ if (isSorted && (pos < s->pieceCount - 1) && (comparePieceByIndex (p, p+1) > 0))
+ isSorted = false;
+ }
+ else
+ {
+ setComparePieceByWeightTorrent (s);
+ if (isSorted && (pos > 0) && (comparePieceByWeight (p-1, p) > 0))
+ isSorted = false;
+ if (isSorted && (pos < s->pieceCount - 1) && (comparePieceByWeight (p, p+1) > 0))
+ isSorted = false;
+ }
- if (s->pieceSortState != PIECES_SORTED_BY_WEIGHT)
+ if (s->pieceSortState != (s->tor->isSequential?PIECES_SORTED_BY_INDEX:PIECES_SORTED_BY_WEIGHT))
{
- pieceListSort (s, PIECES_SORTED_BY_WEIGHT);
+ pieceListSort (s, (s->tor->isSequential?PIECES_SORTED_BY_INDEX:PIECES_SORTED_BY_WEIGHT));
isSorted = true;
}
@@ -1192,7 +1203,7 @@
pos = tr_lowerBound (&tmp, s->pieces, s->pieceCount,
sizeof (struct weighted_piece),
- comparePieceByWeight, &exact);
+ (s->tor->isSequential)?comparePieceByIndex:comparePieceByWeight, &exact);
memmove (&s->pieces[pos + 1],
&s->pieces[pos],
@@ -1238,7 +1249,8 @@
++s->pieceReplication[index];
/* we only resort the piece if the list is already sorted */
- if (s->pieceSortState == PIECES_SORTED_BY_WEIGHT)
+ /* if it sequential, we don't need to resort */
+ if (s->pieceSortState == PIECES_SORTED_BY_WEIGHT && ! s->tor->isSequential)
pieceListResortPiece (s, pieceListLookup (s, index));
}
@@ -1344,8 +1356,8 @@
if (s->pieces == NULL)
pieceListRebuild (s);
- if (s->pieceSortState != PIECES_SORTED_BY_WEIGHT)
- pieceListSort (s, PIECES_SORTED_BY_WEIGHT);
+ if (s->pieceSortState != (s->tor->isSequential?PIECES_SORTED_BY_INDEX:PIECES_SORTED_BY_WEIGHT))
+ pieceListSort (s, (s->tor->isSequential?PIECES_SORTED_BY_INDEX:PIECES_SORTED_BY_WEIGHT));
assertReplicationCountIsExact (s);
assertWeightedPiecesAreSorted (s);
@@ -1381,8 +1393,9 @@
peers = (tr_peer **) tr_ptrArrayPeek (&peerArr, &peerCount);
if (peerCount != 0)
{
+ bool sequentialKickStart = (tor->isSequential && b<((100*1024*1024)/tor->blockCount)); // Prioritize the first 100M
/* don't make a second block request until the endgame */
- if (!s->endgame)
+ if (!sequentialKickStart && !s->endgame)
continue;
/* don't have more than two peers requesting this block */
@@ -1396,8 +1409,9 @@
/* in the endgame allow an additional peer to download a
block but only if the peer seems to be handling requests
relatively fast */
- if (peer->pendingReqsToPeer + numwant - got < s->endgame)
+ if (!sequentialKickStart && peer->pendingReqsToPeer + numwant - got < s->endgame)
continue;
+ tr_logAddDebug("Adding a second peer for block %"PRIu32" for sequential kickstarter %d %d %d",b, sequentialKickStart,tor->blockSize, tor->blockCount);
}
/* update the caller's table */
@@ -1446,7 +1460,7 @@
const int newpos = tr_lowerBound (&s->pieces[i], &s->pieces[i + 1],
s->pieceCount - (i + 1),
sizeof (struct weighted_piece),
- comparePieceByWeight, &exact);
+ (tor->isSequential)?comparePieceByIndex:comparePieceByWeight, &exact);
if (newpos > 0)
{
const struct weighted_piece piece = s->pieces[i];
@@ -1665,6 +1679,13 @@
tr_ptrArrayDestruct (&peerArr, NULL);
}
+tr_file_index_t tr_getFirstPieceIndex(tr_torrent * tor)
+{
+ if (tor->swarm->pieceCount)
+ return tor->swarm->pieces[0].index;
+ else return 0;
+}
+
void
tr_peerMgrPieceCompleted (tr_torrent * tor, tr_piece_index_t p)
{
Index: libtransmission/peer-mgr.h
===================================================================
--- libtransmission/peer-mgr.h (revision 14252)
+++ libtransmission/peer-mgr.h (working copy)
@@ -183,6 +183,9 @@
void tr_peerMgrPieceCompleted (tr_torrent * tor,
tr_piece_index_t pieceIndex);
+
+tr_file_index_t tr_getFirstPieceIndex(tr_torrent * tor);
+
Index: libtransmission/quark.c
===================================================================
--- libtransmission/quark.c (revision 14252)
+++ libtransmission/quark.c (working copy)
@@ -315,6 +315,7 @@
{ "seedRatioMode", 13 },
{ "seederCount", 11 },
{ "seeding-time-seconds", 20 },
+ { "sequential", 10},
{ "session-count", 13 },
{ "sessionCount", 12 },
{ "show-backup-trackers", 20 },
Index: libtransmission/quark.h
===================================================================
--- libtransmission/quark.h (revision 14252)
+++ libtransmission/quark.h (working copy)
@@ -313,6 +313,7 @@
TR_KEY_seedRatioMode,
TR_KEY_seederCount,
TR_KEY_seeding_time_seconds,
+ TR_KEY_sequential,
TR_KEY_session_count,
TR_KEY_sessionCount,
TR_KEY_show_backup_trackers,
Index: libtransmission/resume.c
===================================================================
--- libtransmission/resume.c (revision 14252)
+++ libtransmission/resume.c (working copy)
@@ -662,6 +662,7 @@
tr_variantDictAddInt (&top, TR_KEY_seeding_time_seconds, tor->secondsSeeding);
tr_variantDictAddInt (&top, TR_KEY_downloading_time_seconds, tor->secondsDownloading);
tr_variantDictAddInt (&top, TR_KEY_activity_date, tor->activityDate);
+ tr_variantDictAddBool(&top, TR_KEY_sequential, tor->isSequential);
tr_variantDictAddInt (&top, TR_KEY_added_date, tor->addedDate);
tr_variantDictAddInt (&top, TR_KEY_corrupt, tor->corruptPrev + tor->corruptCur);
tr_variantDictAddInt (&top, TR_KEY_done_date, tor->doneDate);
@@ -799,7 +800,15 @@
tr_torrentSetActivityDate (tor, i);
fieldsLoaded |= TR_FR_ACTIVITY_DATE;
}
+
+ if ((fieldsToLoad & TR_FR_SEQUENTIAL)
+ && tr_variantDictFindBool(&top, TR_KEY_sequential, &boolVal))
+ {
+ tor->isSequential = boolVal;
+ fieldsLoaded |= TR_FR_SEQUENTIAL;
+ }
+
if ((fieldsToLoad & TR_FR_TIME_SEEDING)
&& tr_variantDictFindInt (&top, TR_KEY_seeding_time_seconds, &i))
{
@@ -848,7 +857,7 @@
if (fieldsToLoad & TR_FR_NAME)
fieldsLoaded |= loadName (&top, tor);
-
+
/* loading the resume file triggers of a lot of changes,
* but none of them needs to trigger a re-saving of the
* same resume information... */
Index: libtransmission/resume.h
===================================================================
--- libtransmission/resume.h (revision 14252)
+++ libtransmission/resume.h (working copy)
@@ -38,6 +38,7 @@
TR_FR_TIME_DOWNLOADING = (1 << 19),
TR_FR_FILENAMES = (1 << 20),
TR_FR_NAME = (1 << 21),
+ TR_FR_SEQUENTIAL = (1 << 22),
};
/**
Index: libtransmission/rpcimpl.c
===================================================================
--- libtransmission/rpcimpl.c (revision 14252)
+++ libtransmission/rpcimpl.c (working copy)
@@ -382,26 +382,84 @@
tr_variant * args_out UNUSED,
struct tr_rpc_idle_data * idle_data UNUSED)
{
- int i;
- int torrentCount;
- tr_torrent ** torrents;
+ int i;
+ int torrentCount;
+ tr_torrent ** torrents;
+
+ assert (idle_data == NULL);
+
+ torrents = getTorrents (session, args_in, &torrentCount);
+ for (i=0; i<torrentCount; ++i)
+ {
+ tr_torrent * tor = torrents[i];
+
+ if (tr_torrentCanManualUpdate (tor))
+ {
+ tr_torrentManualUpdate (tor);
+ notify (session, TR_RPC_TORRENT_CHANGED, tor);
+ }
+ }
+
+ tr_free (torrents);
+ return NULL;
+}
- assert (idle_data == NULL);
-
- torrents = getTorrents (session, args_in, &torrentCount);
- for (i=0; i<torrentCount; ++i)
+static const char*
+torrentSetSequential (tr_session * session,
+ tr_variant * args_in,
+ tr_variant * args_out UNUSED,
+ struct tr_rpc_idle_data * idle_data UNUSED)
+{
+ int i;
+ int torrentCount;
+ tr_torrent ** torrents;
+
+ assert (idle_data == NULL);
+
+ torrents = getTorrents (session, args_in, &torrentCount);
+ for (i=0; i<torrentCount; ++i)
{
- tr_torrent * tor = torrents[i];
+ tr_torrent * tor = torrents[i];
- if (tr_torrentCanManualUpdate (tor))
+ if (!tor->isSequential)
{
- tr_torrentManualUpdate (tor);
- notify (session, TR_RPC_TORRENT_CHANGED, tor);
+ tor->isSequential = true;
+ tr_torrentManualUpdate (tor);
+ notify (session, TR_RPC_TORRENT_CHANGED, tor);
}
}
+
+ tr_free (torrents);
+ return NULL;
+}
- tr_free (torrents);
- return NULL;
+static const char*
+torrentUnSetSequential (tr_session * session,
+ tr_variant * args_in,
+ tr_variant * args_out UNUSED,
+ struct tr_rpc_idle_data * idle_data UNUSED)
+{
+ int i;
+ int torrentCount;
+ tr_torrent ** torrents;
+
+ assert (idle_data == NULL);
+
+ torrents = getTorrents (session, args_in, &torrentCount);
+ for (i=0; i<torrentCount; ++i)
+ {
+ tr_torrent * tor = torrents[i];
+
+ if (tor->isSequential)
+ {
+ tor->isSequential = false;
+ tr_torrentManualUpdate (tor);
+ notify (session, TR_RPC_TORRENT_CHANGED, tor);
+ }
+ }
+
+ tr_free (torrents);
+ return NULL;
}
static const char*
@@ -579,13 +637,16 @@
const tr_quark key)
{
char * str;
-
+
switch (key)
{
case TR_KEY_activityDate:
tr_variantDictAddInt (d, key, st->activityDate);
break;
-
+ case TR_KEY_sequential:
+ tr_variantDictAddStr (d, key, st->isSequential ? "Yes" : "No");
+ break;
+
case TR_KEY_addedDate:
tr_variantDictAddInt (d, key, st->addedDate);
break;
@@ -2159,6 +2220,8 @@
{ "torrent-stop", true, torrentStop },
{ "torrent-verify", true, torrentVerify },
{ "torrent-reannounce", true, torrentReannounce },
+ { "torrent-set-sequential",true, torrentSetSequential},
+ { "torrent-unset-sequential",true, torrentUnSetSequential},
{ "queue-move-top", true, queueMoveTop },
{ "queue-move-up", true, queueMoveUp },
{ "queue-move-down", true, queueMoveDown },
Index: libtransmission/torrent.c
===================================================================
--- libtransmission/torrent.c (revision 14252)
+++ libtransmission/torrent.c (working copy)
@@ -1295,6 +1295,7 @@
s->sizeWhenDone = tr_cpSizeWhenDone (&tor->completion);
s->recheckProgress = s->activity == TR_STATUS_CHECK ? getVerifyProgress (tor) : 0;
s->activityDate = tor->activityDate;
+ s->isSequential = tor->isSequential;
s->addedDate = tor->addedDate;
s->doneDate = tor->doneDate;
s->startDate = tor->startDate;
@@ -1642,6 +1643,7 @@
now = tr_time ();
tor->isRunning = true;
+ tor->isSequential = false;
tor->completeness = tr_cpGetStatus (&tor->completion);
tor->startDate = tor->anyDate = now;
tr_torrentClearError (tor);
@@ -3239,15 +3241,16 @@
if (block_is_new)
{
- tr_piece_index_t p;
+ tr_piece_index_t p, p2;
tr_cpBlockAdd (&tor->completion, block);
tr_torrentSetDirty (tor);
p = tr_torBlockPiece (tor, block);
+ p2 = tr_getFirstPieceIndex(tor);
if (tr_torrentPieceIsComplete (tor, p))
{
- tr_logAddTorDbg (tor, "[LAZY] checking just-completed piece %"TR_PRIuSIZE, (size_t)p);
+ tr_logAddTorDbg (tor, "[LAZY] checking just-completed piece %"TR_PRIuSIZE", largest completed piece is %"TR_PRIuSIZE, (size_t)p, (size_t)p2);
if (tr_torrentCheckPiece (tor, p))
{
Index: libtransmission/torrent.h
===================================================================
--- libtransmission/torrent.h (revision 14252)
+++ libtransmission/torrent.h (working copy)
@@ -246,6 +246,7 @@
bool startAfterVerify;
bool isDirty;
bool isQueued;
+ bool isSequential;
bool magnetVerify;
@@ -498,6 +499,11 @@
return tr_cpHaveTotal (&tor->completion);
}
+static inline bool
+tr_torrentIsSequential (const tr_torrent * tor)
+{
+ return tor->isSequential;
+}
static inline bool
tr_torrentIsQueued (const tr_torrent * tor)
Index: libtransmission/transmission.h
===================================================================
--- libtransmission/transmission.h (revision 14252)
+++ libtransmission/transmission.h (working copy)
@@ -2055,6 +2055,9 @@
/** True if the torrent is running, but has been idle for long enough
to be considered stalled. @see tr_sessionGetQueueStalledMinutes () */
bool isStalled;
+
+ /** True if the torrent is downloading in sequential mode */
+ bool isSequential;
}
tr_stat;
Index: web/index.html
===================================================================
--- web/index.html (revision 14252)
+++ web/index.html (working copy)
@@ -208,6 +208,7 @@
<div class="row"><div class="key">Running Time:</div><div class="value" id="inspector-info-running-time">&nbsp;</div></div>
<div class="row"><div class="key">Remaining Time:</div><div class="value" id="inspector-info-remaining-time">&nbsp;</div></div>
<div class="row"><div class="key">Last Activity:</div><div class="value" id="inspector-info-last-activity">&nbsp;</div></div>
+ <div class="row"><div class="key">Sequential:</div><div class="value" id="inspector-info-sequential">&nbsp;</div></div>
<div class="row"><div class="key">Error:</div><div class="value" id="inspector-info-error">&nbsp;</div></div>
</div>
<div class="prefs-section">
@@ -425,6 +426,9 @@
<li id="context_move">Set Location…</li>
<li id="context_rename">Rename…</li>
<li class="separator"></li>
+ <li id="context_set_sequential">Set torrent to sequential mode</li>
+ <li id="context_unset_sequential">Set torrent to non-sequential mode</li>
+ <li class="separator"></li>
<li id="context_reannounce">Ask tracker for more peers</li>
<li class="separator"></li>
<li id="context_select_all">Select All</li>
Index: web/javascript/inspector.js
===================================================================
--- web/javascript/inspector.js (revision 14252)
+++ web/javascript/inspector.js (working copy)
@@ -301,6 +301,25 @@
str = fmt.timeInterval(d) + ' ago';
}
setTextContent(e.last_activity_lb, str);
+
+ //
+ // sequential
+ //
+
+ if(torrents.length < 1)
+ str = none;
+ else {
+ str = torrents[0].getSequential();
+ for(i=0; t=torrents[i]; ++i) {
+ if(str != t.getSequential()) {
+ str = mixed;
+ break;
+ }
+ }
+ }
+ if(!str)
+ str = none;
+ setTextContent(e.sequential_lb, str);
//
// error
@@ -774,6 +793,7 @@
data.elements.running_time_lb = $('#inspector-info-running-time')[0];
data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
data.elements.last_activity_lb = $('#inspector-info-last-activity')[0];
+ data.elements.sequential_lb = $('#inspector-info-sequential')[0];
data.elements.error_lb = $('#inspector-info-error')[0];
data.elements.size_lb = $('#inspector-info-size')[0];
data.elements.foldername_lb = $('#inspector-info-location')[0];
Index: web/javascript/remote.js
===================================================================
--- web/javascript/remote.js (revision 14252)
+++ web/javascript/remote.js (working copy)
@@ -215,6 +215,12 @@
reannounceTorrents: function(torrent_ids, callback, context) {
this.sendTorrentActionRequests('torrent-reannounce', torrent_ids, callback, context);
},
+ setSequentialTorrents: function(torrent_ids, callback, context) {
+ this.sendTorrentActionRequests('torrent-set-sequential', torrent_ids, callback, context);
+ },
+ unsetSequentialTorrents: function(torrent_ids, callback, context) {
+ this.sendTorrentActionRequests('torrent-unset-sequential', torrent_ids, callback, context);
+ },
addTorrentByUrl: function(url, options) {
var remote = this;
if (url.match(/^[0-9a-f]{40}$/i)) {
Index: web/javascript/torrent.js
===================================================================
--- web/javascript/torrent.js (revision 14252)
+++ web/javascript/torrent.js (working copy)
@@ -97,6 +97,7 @@
// fields used in the inspector which need to be periodically refreshed
Torrent.Fields.StatsExtra = [
'activityDate',
+ 'sequential',
'corruptEver',
'desiredAvailable',
'downloadedEver',
@@ -228,6 +229,7 @@
getHaveValid: function() { return this.fields.haveValid; },
getId: function() { return this.fields.id; },
getLastActivity: function() { return this.fields.activityDate; },
+ getSequential: function() { return this.fields.sequential; },
getLeftUntilDone: function() { return this.fields.leftUntilDone; },
getMetadataPercentComplete: function() { return this.fields.metadataPercentComplete; },
getName: function() { return this.fields.name || 'Unknown'; },
Index: web/javascript/transmission.js
===================================================================
--- web/javascript/transmission.js (revision 14252)
+++ web/javascript/transmission.js (working copy)
@@ -189,6 +189,8 @@
context_verify: function() { tr.verifySelectedTorrents(); },
context_rename: function() { tr.renameSelectedTorrents(); },
context_reannounce: function() { tr.reannounceSelectedTorrents(); },
+ context_set_sequential: function() { tr.setSequentialSelectedTorrents(); },
+ context_unset_sequential: function() { tr.unsetSequentialSelectedTorrents(); },
context_move_top: function() { tr.moveTop(); },
context_move_up: function() { tr.moveUp(); },
context_move_down: function() { tr.moveDown(); },
@@ -1107,6 +1109,14 @@
reannounceSelectedTorrents: function() {
this.reannounceTorrents(this.getSelectedTorrents());
},
+
+ setSequentialSelectedTorrents: function() {
+ this.setSequentialTorrents(this.getSelectedTorrents());
+ },
+
+ unsetSequentialSelectedTorrents: function() {
+ this.unsetSequentialTorrents(this.getSelectedTorrents());
+ },
startAllTorrents: function(force) {
this.startTorrents(this.getAllTorrents(), force);
@@ -1138,6 +1148,22 @@
this.refreshTorrents, this);
},
+ setSequentialTorrent: function(torrent) {
+ this.setSequentialTorrents([ torrent ]);
+ },
+ setSequentialTorrents: function(torrents) {
+ this.remote.setSequentialTorrents(this.getTorrentIds(torrents),
+ this.refreshTorrents, this);
+ },
+
+ unsetSequentialTorrent: function(torrent) {
+ this.unsetSequentialTorrents([ torrent ]);
+ },
+ unsetSequentialTorrents: function(torrents) {
+ this.remote.unsetSequentialTorrents(this.getTorrentIds(torrents),
+ this.refreshTorrents, this);
+ },
+
stopAllTorrents: function() {
this.stopTorrents(this.getAllTorrents());
},
@nelsonjchen
Copy link

I've heard that an alternative that's not as destructive to the torrent swarm health is sequential prioritizing with deadlines of pieces. Oh well, this is over my head.

@jhenkens
Copy link
Author

jhenkens commented Feb 8, 2015

@nelsonjchecn I never noticed that you commented on this! I wish gists would send emails....

I hardly ever use my sequential patch - it is not the default download option, and I have to manually set it for each torrent. It has worked very well, although occasionally transmission does crash while running in the background (I have no idea why - I'm thinking of updating this patch to use a current version of the source). I use transmission on a headless server, because I find most torrent clients way to bloated with features.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment