From aedf579d772210aa0f4c951eb3462fadda3af97c Mon Sep 17 00:00:00 2001 From: Gabriele Date: Sat, 8 Nov 2014 16:13:00 +0100 Subject: [PATCH] WebUI: make API locale independet Sizes are now given in bytes. Dates are Unix timestamps and converted to ISO 8601 in the web UI. Numbers are not converted to strings. -1 is returned for undefined values. Some keys have been splitted: Torrent list (json/torrents) * num_seeds: Torrent seeds connected to * num_complete: Torrent seeds in the swarm * num_leechs: Torrent leechers connected to * num_incomplete: Torrent leechers in the swarm Torrent generic properties (propertiesGeneral/hash) * total_uploaded: Total data uploaded * total_uploaded_session: Total data uploaded this session * total_downloaded: Total data dowloaded * total_downloaded_session: Total data downloaded this session * time_elapsed: Torrent elapsed time * seeding_time: Torrent elapsed time while complete * nb_connections: Torrent connection count * nb_connections_limit: Torrent connection count limit Global transfer info (json/transferInfo) * dl_info_speed: Global downalod rate * dl_info_data: Data downloaded this session * up_info_speed: Global upload rate * up_info_data: Data uploaded this session Closes #1524. --- src/qtlibtorrent/qtorrenthandle.cpp | 9 +++ src/qtlibtorrent/qtorrenthandle.h | 1 + src/webui/btjson.cpp | 100 ++++++++++++++----------- src/webui/webui.qrc | 1 + src/webui/www/private/index.html | 1 + src/webui/www/public/prop-files.html | 2 +- src/webui/www/public/prop-general.html | 33 +++++--- src/webui/www/public/scripts/client.js | 31 +++++--- src/webui/www/public/scripts/misc.js | 78 +++++++++++++++++++ 9 files changed, 192 insertions(+), 64 deletions(-) create mode 100644 src/webui/www/public/scripts/misc.js diff --git a/src/qtlibtorrent/qtorrenthandle.cpp b/src/qtlibtorrent/qtorrenthandle.cpp index cedeb76ac..288d823d2 100644 --- a/src/qtlibtorrent/qtorrenthandle.cpp +++ b/src/qtlibtorrent/qtorrenthandle.cpp @@ -103,6 +103,15 @@ QString QTorrentHandle::creation_date() const { return t ? misc::toQString(*t) : ""; } +qlonglong QTorrentHandle::creation_date_unix() const { +#if LIBTORRENT_VERSION_NUM < 10000 + boost::optional t = torrent_handle::get_torrent_info().creation_date(); +#else + boost::optional t = torrent_handle::torrent_file()->creation_date(); +#endif + return t ? *t : -1; +} + QString QTorrentHandle::current_tracker() const { return misc::toQString(status(0x0).current_tracker); } diff --git a/src/qtlibtorrent/qtorrenthandle.h b/src/qtlibtorrent/qtorrenthandle.h index a41be30c2..ca7278cc0 100644 --- a/src/qtlibtorrent/qtorrenthandle.h +++ b/src/qtlibtorrent/qtorrenthandle.h @@ -84,6 +84,7 @@ public: bool is_checking() const; bool is_sequential_download() const; QString creation_date() const; + qlonglong creation_date_unix() const; bool priv() const; bool first_last_piece_first() const; QString root_path() const; diff --git a/src/webui/btjson.cpp b/src/webui/btjson.cpp index 4272e1650..5faa1f4c3 100644 --- a/src/webui/btjson.cpp +++ b/src/webui/btjson.cpp @@ -29,7 +29,6 @@ */ #include "btjson.h" -#include "misc.h" #include "fs_utils.h" #include "qbtsession.h" #include "torrentpersistentdata.h" @@ -89,7 +88,9 @@ static const char KEY_TORRENT_DLSPEED[] = "dlspeed"; static const char KEY_TORRENT_UPSPEED[] = "upspeed"; static const char KEY_TORRENT_PRIORITY[] = "priority"; static const char KEY_TORRENT_SEEDS[] = "num_seeds"; +static const char KEY_TORRENT_NUM_COMPLETE[] = "num_complete"; static const char KEY_TORRENT_LEECHS[] = "num_leechs"; +static const char KEY_TORRENT_NUM_INCOMPLETE[] = "num_incomplete"; static const char KEY_TORRENT_RATIO[] = "ratio"; static const char KEY_TORRENT_ETA[] = "eta"; static const char KEY_TORRENT_STATE[] = "state"; @@ -107,11 +108,15 @@ static const char KEY_PROP_PIECE_SIZE[] = "piece_size"; static const char KEY_PROP_COMMENT[] = "comment"; static const char KEY_PROP_WASTED[] = "total_wasted"; static const char KEY_PROP_UPLOADED[] = "total_uploaded"; +static const char KEY_PROP_UPLOADED_SESSION[] = "total_uploaded_session"; static const char KEY_PROP_DOWNLOADED[] = "total_downloaded"; +static const char KEY_PROP_DOWNLOADED_SESSION[] = "total_downloaded_session"; static const char KEY_PROP_UP_LIMIT[] = "up_limit"; static const char KEY_PROP_DL_LIMIT[] = "dl_limit"; static const char KEY_PROP_TIME_ELAPSED[] = "time_elapsed"; +static const char KEY_PROP_SEEDING_TIME[] = "seeding_time"; static const char KEY_PROP_CONNECT_COUNT[] = "nb_connections"; +static const char KEY_PROP_CONNECT_COUNT_LIMIT[] = "nb_connections_limit"; static const char KEY_PROP_RATIO[] = "share_ratio"; // File keys @@ -122,8 +127,10 @@ static const char KEY_FILE_PRIORITY[] = "priority"; static const char KEY_FILE_IS_SEED[] = "is_seed"; // TransferInfo keys -static const char KEY_TRANSFER_DLSPEED[] = "dl_info"; -static const char KEY_TRANSFER_UPSPEED[] = "up_info"; +static const char KEY_TRANSFER_DLSPEED[] = "dl_info_speed"; +static const char KEY_TRANSFER_DLDATA[] = "dl_info_data"; +static const char KEY_TRANSFER_UPSPEED[] = "up_info_speed"; +static const char KEY_TRANSFER_UPDATA[] = "up_info_data"; static QVariantMap toMap(const QTorrentHandle& h) { @@ -132,25 +139,21 @@ static QVariantMap toMap(const QTorrentHandle& h) QVariantMap ret; ret[KEY_TORRENT_HASH] = h.hash(); ret[KEY_TORRENT_NAME] = h.name(); - ret[KEY_TORRENT_SIZE] = misc::friendlyUnit(status.total_wanted, false, true); // FIXME: Should pass as Number, not formatted String (for sorting). - ret[KEY_TORRENT_PROGRESS] = (double)h.progress(status); - ret[KEY_TORRENT_DLSPEED] = misc::friendlyUnit(status.download_payload_rate, true, true); // FIXME: Should be passed as a Number - ret[KEY_TORRENT_UPSPEED] = misc::friendlyUnit(status.upload_payload_rate, true, true); // FIXME: Should be passed as a Number + ret[KEY_TORRENT_SIZE] = static_cast(status.total_wanted); + ret[KEY_TORRENT_PROGRESS] = h.progress(status); + ret[KEY_TORRENT_DLSPEED] = status.download_payload_rate; + ret[KEY_TORRENT_UPSPEED] = status.upload_payload_rate; if (QBtSession::instance()->isQueueingEnabled() && h.queue_position(status) >= 0) - ret[KEY_TORRENT_PRIORITY] = QString::number(h.queue_position(status)); + ret[KEY_TORRENT_PRIORITY] = h.queue_position(status); else - ret[KEY_TORRENT_PRIORITY] = "*"; - QString seeds = QString::number(status.num_seeds); - if (status.num_complete > 0) - seeds += " ("+QString::number(status.num_complete)+")"; - ret[KEY_TORRENT_SEEDS] = seeds; - QString leechs = QString::number(status.num_peers - status.num_seeds); - if (status.num_incomplete > 0) - leechs += " ("+QString::number(status.num_incomplete)+")"; - ret[KEY_TORRENT_LEECHS] = leechs; + ret[KEY_TORRENT_PRIORITY] = -1; + ret[KEY_TORRENT_SEEDS] = status.num_seeds; + ret[KEY_TORRENT_NUM_COMPLETE] = status.num_complete; + ret[KEY_TORRENT_LEECHS] = status.num_peers - status.num_seeds; + ret[KEY_TORRENT_NUM_INCOMPLETE] = status.num_incomplete; const qreal ratio = QBtSession::instance()->getRealRatio(status); - ret[KEY_TORRENT_RATIO] = (ratio > 100.) ? QString::fromUtf8("∞") : misc::accurateDoubleToString(ratio, 1, false); - QString eta; + ret[KEY_TORRENT_RATIO] = (ratio > 100.) ? -1 : ratio; + qulonglong eta = 0; QString state; if (h.is_paused(status)) { if (h.has_error(status)) @@ -175,14 +178,14 @@ static QVariantMap toMap(const QTorrentHandle& h) case torrent_status::downloading: case torrent_status::downloading_metadata: state = status.download_payload_rate > 0 ? "downloading" : "stalledDL"; - eta = misc::userFriendlyDuration(QBtSession::instance()->getETA(h.hash(), status)); + eta = QBtSession::instance()->getETA(h.hash(), status); break; default: qWarning("Unrecognized torrent status, should not happen!!! status was %d", h.state()); } } } - ret[KEY_TORRENT_ETA] = eta.isEmpty() ? QString::fromUtf8("∞") : eta; + ret[KEY_TORRENT_ETA] = eta ? eta : MAX_ETA; ret[KEY_TORRENT_STATE] = state; return ret; @@ -199,9 +202,11 @@ static QVariantMap toMap(const QTorrentHandle& h) * - "progress: Torrent progress * - "dlspeed": Torrent download speed * - "upspeed": Torrent upload speed - * - "priority": Torrent priority ('*' if queuing is disabled) - * - "num_seeds": Torrent seed count - * - "num_leechs": Torrent leecher count + * - "priority": Torrent priority (-1 if queuing is disabled) + * - "num_seeds": Torrent seeds connected to + * - "num_complete": Torrent seeds in the swarm + * - "num_leechs": Torrent leechers connected to + * - "num_incomplete": Torrent leechers in the swarm * - "ratio": Torrent share ratio * - "eta": Torrent ETA * - "state": Torrent state @@ -252,7 +257,7 @@ QByteArray btjson::getTrackersForTorrent(const QString& hash) status = it->fails > 0 ? tr("Not working") : tr("Not contacted yet"); } tracker_dict[KEY_TRACKER_STATUS] = status; - tracker_dict[KEY_TRACKER_PEERS] = QString::number(trackers_data.value(tracker_url, TrackerInfos(tracker_url)).num_peers); + tracker_dict[KEY_TRACKER_PEERS] = static_cast(trackers_data.value(tracker_url, TrackerInfos(tracker_url)).num_peers); tracker_dict[KEY_TRACKER_MSG] = data.last_message.trimmed(); tracker_list.append(tracker_dict); @@ -276,11 +281,15 @@ QByteArray btjson::getTrackersForTorrent(const QString& hash) * - "comment": Torrent comment * - "total_wasted": Total data wasted for torrent * - "total_uploaded": Total data uploaded for torrent + * - "total_uploaded_session": Total data uploaded this session * - "total_downloaded": Total data uploaded for torrent + * - "total_downloaded_session": Total data downloaded this session * - "up_limit": Torrent upload limit * - "dl_limit": Torrent download limit * - "time_elapsed": Torrent elapsed time + * - "seeding_time": Torrent elapsed time while complete * - "nb_connections": Torrent connection count + * - "nb_connections_limit": Torrent connection count limit * - "share_ratio": Torrent share ratio */ QByteArray btjson::getPropertiesForTorrent(const QString& hash) @@ -298,21 +307,22 @@ QByteArray btjson::getPropertiesForTorrent(const QString& hash) if (save_path.isEmpty()) save_path = fsutils::toNativePath(h.save_path()); data[KEY_PROP_SAVE_PATH] = save_path; - data[KEY_PROP_CREATION_DATE] = h.creation_date(); - data[KEY_PROP_PIECE_SIZE] = misc::friendlyUnit(h.piece_length(), false, true); + data[KEY_PROP_CREATION_DATE] = h.creation_date_unix(); + data[KEY_PROP_PIECE_SIZE] = static_cast(h.piece_length()); data[KEY_PROP_COMMENT] = h.comment(); - data[KEY_PROP_WASTED] = misc::friendlyUnit(status.total_failed_bytes + status.total_redundant_bytes, false, true); - data[KEY_PROP_UPLOADED] = QString(misc::friendlyUnit(status.all_time_upload, false, true) + " (" + misc::friendlyUnit(status.total_payload_upload, false, true) + " " + tr("this session") + ")"); - data[KEY_PROP_DOWNLOADED] = QString(misc::friendlyUnit(status.all_time_download, false, true) + " (" + misc::friendlyUnit(status.total_payload_download, false, true) + " " + tr("this session") + ")"); - data[KEY_PROP_UP_LIMIT] = h.upload_limit() <= 0 ? QString::fromUtf8("∞") : misc::friendlyUnit(h.upload_limit(), true, true); - data[KEY_PROP_DL_LIMIT] = h.download_limit() <= 0 ? QString::fromUtf8("∞") : misc::friendlyUnit(h.download_limit(), true, true); - QString elapsed_txt = misc::userFriendlyDuration(status.active_time); - if (h.is_seed(status)) - elapsed_txt += " ("+tr("Seeded for %1", "e.g. Seeded for 3m10s").arg(misc::userFriendlyDuration(status.seeding_time))+")"; - data[KEY_PROP_TIME_ELAPSED] = elapsed_txt; - data[KEY_PROP_CONNECT_COUNT] = QString(QString::number(status.num_connections) + " (" + tr("%1 max", "e.g. 10 max").arg(QString::number(status.connections_limit)) + ")"); + data[KEY_PROP_WASTED] = static_cast(status.total_failed_bytes + status.total_redundant_bytes); + data[KEY_PROP_UPLOADED] = static_cast(status.all_time_upload); + data[KEY_PROP_UPLOADED_SESSION] = static_cast(status.total_payload_upload); + data[KEY_PROP_DOWNLOADED] = static_cast(status.all_time_download); + data[KEY_PROP_DOWNLOADED_SESSION] = static_cast(status.total_payload_download); + data[KEY_PROP_UP_LIMIT] = h.upload_limit() <= 0 ? -1 : h.upload_limit(); + data[KEY_PROP_DL_LIMIT] = h.download_limit() <= 0 ? -1 : h.download_limit(); + data[KEY_PROP_TIME_ELAPSED] = status.active_time; + data[KEY_PROP_SEEDING_TIME] = status.seeding_time; + data[KEY_PROP_CONNECT_COUNT] = status.num_connections; + data[KEY_PROP_CONNECT_COUNT_LIMIT] = status.connections_limit; const qreal ratio = QBtSession::instance()->getRealRatio(status); - data[KEY_PROP_RATIO] = ratio > 100. ? QString::fromUtf8("∞") : misc::accurateDoubleToString(ratio, 1, false); + data[KEY_PROP_RATIO] = ratio > 100. ? -1 : ratio; } catch(const std::exception& e) { qWarning() << Q_FUNC_INFO << "Invalid torrent: " << misc::toQStringU(e.what()); return QByteArray(); @@ -350,7 +360,7 @@ QByteArray btjson::getFilesForTorrent(const QString& hash) fileName.chop(4); file_dict[KEY_FILE_NAME] = fsutils::toNativePath(fileName); const size_type size = h.filesize_at(i); - file_dict[KEY_FILE_SIZE] = misc::friendlyUnit(size, false, true); + file_dict[KEY_FILE_SIZE] = static_cast(size); file_dict[KEY_FILE_PROGRESS] = (size > 0) ? (fp[i] / (double) size) : 1.; file_dict[KEY_FILE_PRIORITY] = priorities[i]; if (i == 0) @@ -371,14 +381,18 @@ QByteArray btjson::getFilesForTorrent(const QString& hash) * * The return value is a JSON-formatted dictionary. * The dictionary keys are: - * - "dl_info": Global download info - * - "up_info": Global upload info + * - "dl_info_speed": Global download rate + * - "dl_info_data": Data downloaded this session + * - "up_info_speed": Global upload rate + * - "up_info_data": Data uploaded this session */ QByteArray btjson::getTransferInfo() { CACHED_VARIABLE(QVariantMap, info, CACHE_DURATION_MS); session_status sessionStatus = QBtSession::instance()->getSessionStatus(); - info[KEY_TRANSFER_DLSPEED] = tr("D: %1/s - T: %2", "Download speed: x KiB/s - Transferred: x MiB").arg(misc::friendlyUnit(sessionStatus.payload_download_rate, true, true)).arg(misc::friendlyUnit(sessionStatus.total_payload_download, false, true)); - info[KEY_TRANSFER_UPSPEED] = tr("U: %1/s - T: %2", "Upload speed: x KiB/s - Transferred: x MiB").arg(misc::friendlyUnit(sessionStatus.payload_upload_rate, true, true)).arg(misc::friendlyUnit(sessionStatus.total_payload_upload, false, true)); + info[KEY_TRANSFER_DLSPEED] = sessionStatus.payload_download_rate; + info[KEY_TRANSFER_DLDATA] = static_cast(sessionStatus.total_payload_download); + info[KEY_TRANSFER_UPSPEED] = sessionStatus.payload_upload_rate; + info[KEY_TRANSFER_UPDATA] = static_cast(sessionStatus.total_payload_upload); return json::toJson(info); } diff --git a/src/webui/webui.qrc b/src/webui/webui.qrc index 035301d86..f0b7a13b8 100644 --- a/src/webui/webui.qrc +++ b/src/webui/webui.qrc @@ -13,6 +13,7 @@ www/public/scripts/download.js www/public/scripts/dynamicTable.js www/public/scripts/excanvas-compressed.js + www/public/scripts/misc.js www/public/scripts/mocha.js www/public/scripts/mocha-init.js www/public/scripts/mocha-yc.js diff --git a/src/webui/www/private/index.html b/src/webui/www/private/index.html index 80f10c32e..c8e246073 100644 --- a/src/webui/www/private/index.html +++ b/src/webui/www/private/index.html @@ -18,6 +18,7 @@ + diff --git a/src/webui/www/public/prop-files.html b/src/webui/www/public/prop-files.html index 9fdd9b2ad..a5ce18d9b 100644 --- a/src/webui/www/public/prop-files.html +++ b/src/webui/www/public/prop-files.html @@ -303,7 +303,7 @@ var createPriorityCombo = function(id, selected_prio) { row.length = 4; row[0] = file.priority; row[1] = file.name; - row[2] = file.size; + row[2] = friendlyUnit(file.size, false); row[3] = (file.progress*100).round(1); if(row[3] == 100.0 && file.progress < 1.0) row[3] = 99.9 diff --git a/src/webui/www/public/prop-general.html b/src/webui/www/public/prop-general.html index a8283ff79..029312dbe 100644 --- a/src/webui/www/public/prop-general.html +++ b/src/webui/www/public/prop-general.html @@ -80,19 +80,34 @@ dynamic information: total_downloaded, total_uploaded, total_wasted, up_limit, d onSuccess: function(data) { $('error_div').set('html', ''); if(data){ + var temp; // Update Torrent data $('save_path').set('html', data.save_path); - $('creation_date').set('html', data.creation_date); - $('piece_size').set('html', data.piece_size); + temp = data.creation_date; + var timestamp = "_(Unknown)"; + if (temp != -1) + timestamp = new Date(data.creation_date*1000).toISOString(); + $('creation_date').set('html', timestamp); + $('piece_size').set('html', friendlyUnit(data.piece_size)); $('comment').set('html', data.comment); - $('total_uploaded').set('html', data.total_uploaded); - $('total_downloaded').set('html', data.total_downloaded); + $('total_uploaded').set('html', friendlyUnit(data.total_uploaded) + + " (" + friendlyUnit(data.total_uploaded_session) + + " (" + "_(this session)" + ")"); + $('total_downloaded').set('html', friendlyUnit(data.total_downloaded) + + " (" + friendlyUnit(data.total_downloaded_session) + + " (" + "_(this session)" + ")"); $('total_wasted').set('html', data.total_wasted); - $('up_limit').set('html', data.up_limit); - $('dl_limit').set('html', data.dl_limit); - $('time_elapsed').set('html', data.time_elapsed); - $('nb_connections').set('html', data.nb_connections); - $('share_ratio').set('html', data.share_ratio); + temp = data.up_limit; + $('up_limit').set('html', temp == -1 ? "∞" : temp); + temp = data.dl_limit; + $('dl_limit').set('html', temp == -1 ? "∞" : temp); + temp = friendlyDuration(status.active_time); + if (status.is_seed) + temp += " (" + "_(Seeded for %1)".replace("%1", status.seeding_time) + ")"; + $('time_elapsed').set('html', temp); + temp = data.nb_connections + " (" + "_(%1 max)".replace("%1", status.nb_connections_limit) + ")"; + $('nb_connections').set('html', temp); + $('share_ratio').set('html', data.share_ratio.toFixed(2)); } else { clearData(); } diff --git a/src/webui/www/public/scripts/client.js b/src/webui/www/public/scripts/client.js index 85b100a96..d01b89546 100644 --- a/src/webui/www/public/scripts/client.js +++ b/src/webui/www/public/scripts/client.js @@ -108,8 +108,10 @@ window.addEvent('load', function(){ }, onSuccess: function(info) { if(info) { - $("DlInfos").set('html', info.dl_info); - $("UpInfos").set('html', info.up_info); + $("DlInfos").set('html', "_(D: %1 - T: %2)".replace("%1", friendlyUnit(info.dl_info_speed, true)) + .replace("%2", friendlyUnit(info.dl_info_data, false))); + $("UpInfos").set('html', "_(U: %1 - T: %2)".replace("%1", friendlyUnit(info.up_info_speed, true)) + .replace("%2", friendlyUnit(info.up_info_data, false))); waitingTrInfo=false; loadTransferInfo.delay(3000); } @@ -146,18 +148,25 @@ window.addEvent('load', function(){ row.length = 10; row[0] = stateToImg(event.state); row[1] = event.name; - row[2] = event.priority - row[3] = event.size; + row[2] = event.priority > -1 ? event.priority : null; + row[3] = friendlyUnit(event.size, false); row[4] = (event.progress*100).round(1); if(row[4] == 100.0 && event.progress != 1.0) row[4] = 99.9; - row[5] = event.num_seeds; - row[6] = event.num_leechs; - row[7] = event.dlspeed; - row[8] = event.upspeed; - row[9] = event.eta; - row[10] = event.ratio; - if(row[2] != "*") + row[5] = event.num_seeds; + if (event.num_complete != -1) + row[5] += " (" + event.num_complete + ")"; + row[6] = event.num_leechs; + if (event.num_incomplete != -1) + row[6] += " (" + event.num_incomplete + ")"; + row[7] = friendlyUnit(event.dlspeed, true); + row[8] = friendlyUnit(event.upspeed, true); + row[9] = friendlyDuration(event.eta); + if(event.ratio == -1) + row[10] = "∞"; + else + row[10] = (Math.floor(100 * event.ratio) / 100).toFixed(2); //Don't round up + if(row[2] != null) queueing_enabled = true; if(!torrent_hashes.contains(event.hash)) { // New unfinished torrent diff --git a/src/webui/www/public/scripts/misc.js b/src/webui/www/public/scripts/misc.js new file mode 100644 index 000000000..b547a621a --- /dev/null +++ b/src/webui/www/public/scripts/misc.js @@ -0,0 +1,78 @@ +/* + * JS counterpart of the function in src/misc.cpp + */ +function friendlyUnit(value, isSpeed) { + units = [ + "_(B)", + "_(KiB)", + "_(MiB)", + "_(GiB)", + "_(TiB)", + ]; + + if (value < 0) + return "_(Unknown)"; + var i = 0; + while (value >= 1024. && i++ < 6) + value /= 1024.; + var ret; + if (i == 0) + ret = value.toFixed(2) + " " + units[0]; + else + ret = value.toFixed(2) + " " + units[i]; + if (isSpeed) + ret += "_(/s)"; + return ret; +} + +/* + * JS counterpart of the function in src/misc.cpp + */ +function friendlyDuration(seconds) { + var MAX_ETA = 8640000; + if (seconds < 0 || seconds >= MAX_ETA) + return "∞"; + if (seconds == 0) + return "0"; + if (seconds < 60) + return "< " + "_(%1m)".replace("%1", "1"); //translation of "< 1m" not working + var minutes = seconds / 60; + if (minutes < 60) + return "_(%1m)".replace("%1", parseInt(minutes)); + var hours = minutes / 60; + minutes = minutes - hours*60; + if (hours < 24) + return "_(%1h %2m)".replace("%1", parseInt(hours)).replace("%2", parseInt(minutes)) + var days = hours / 24; + hours = hours - days * 24; + if (days < 100) + return "_(%1d %2h)".replace("%1", parseInt(days)).replace("%2", parseInt(hours)) + return "∞"; +} + +/* + * From: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + */ +if (!Date.prototype.toISOString) { + (function() { + + function pad(number) { + if (number < 10) { + return '0' + number; + } + return number; + } + + Date.prototype.toISOString = function() { + return this.getUTCFullYear() + + '-' + pad(this.getUTCMonth() + 1) + + '-' + pad(this.getUTCDate()) + + 'T' + pad(this.getUTCHours()) + + ':' + pad(this.getUTCMinutes()) + + ':' + pad(this.getUTCSeconds()) + + '.' + (this.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) + + 'Z'; + }; + + }()); +} \ No newline at end of file