From 5499be89203a18ca61a21cfc7266cf0f4ebe6547 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Thu, 15 Dec 2011 23:18:21 +0100 Subject: refractoring --- module/PyFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'module/PyFile.py') diff --git a/module/PyFile.py b/module/PyFile.py index 3dede9360..e2d906705 100644 --- a/module/PyFile.py +++ b/module/PyFile.py @@ -17,7 +17,7 @@ @author: mkaay """ -from module.PullEvents import UpdateEvent +from interaction.PullEvents import UpdateEvent from module.utils import formatSize, lock from time import sleep, time -- cgit v1.2.3 From d35c003cc53d4723d1dfe0d81eeb9bea78cee594 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Sat, 31 Dec 2011 16:01:24 +0100 Subject: new crypter plugin API, now decrypting possible for now. --- module/PyFile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'module/PyFile.py') diff --git a/module/PyFile.py b/module/PyFile.py index e2d906705..dae61e361 100644 --- a/module/PyFile.py +++ b/module/PyFile.py @@ -276,8 +276,7 @@ class PyFile(object): return self.size def notifyChange(self): - e = UpdateEvent("file", self.id, "collector" if not self.package().queue else "queue") - self.m.core.pullManager.addEvent(e) + self.m.core.eventManager.dispatchEvent("linkUpdated", self.id, self.packageid) def setProgress(self, value): if not value == self.progress: -- cgit v1.2.3 From 5a3e5a8228e4c5421b44d18c9c9ae2f1fe616400 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Sun, 1 Jan 2012 18:01:25 +0100 Subject: fixed imports --- module/PyFile.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'module/PyFile.py') diff --git a/module/PyFile.py b/module/PyFile.py index dae61e361..b446fde65 100644 --- a/module/PyFile.py +++ b/module/PyFile.py @@ -17,13 +17,12 @@ @author: mkaay """ -from interaction.PullEvents import UpdateEvent -from module.utils import formatSize, lock from time import sleep, time - from threading import RLock +from module.utils import formatSize, lock + statusMap = { "finished": 0, "offline": 1, -- cgit v1.2.3 From bac28b7740aae772636d8b90e291d9c17dfd59a7 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Sun, 8 Jan 2012 14:44:59 +0100 Subject: new MultiHoster hook --- module/PyFile.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'module/PyFile.py') diff --git a/module/PyFile.py b/module/PyFile.py index b446fde65..0c4c20705 100644 --- a/module/PyFile.py +++ b/module/PyFile.py @@ -51,7 +51,7 @@ class PyFile(object): """ __slots__ = ("m", "id", "url", "name", "size", "_size", "status", "pluginname", "packageid", "error", "order", "lock", "plugin", "waitUntil", "active", "abort", "statusname", - "reconnected", "progress", "maxprogress", "pluginmodule", "pluginclass") + "reconnected", "progress", "maxprogress", "pluginclass") def __init__(self, manager, id, url, name, size, status, error, pluginname, package, order): self.m = manager @@ -91,14 +91,13 @@ class PyFile(object): size = property(lambda self: self._size, setSize) def __repr__(self): - return "PyFile %s: %s@%s" % (self.id, self.name, self.pluginname) + return "" % (self.id, self.name, self.pluginname) @lock def initPlugin(self): """ inits plugin instance """ if not self.plugin: - self.pluginmodule = self.m.core.pluginManager.getPlugin(self.pluginname) - self.pluginclass = getattr(self.pluginmodule, self.m.core.pluginManager.getPluginName(self.pluginname)) + self.pluginclass = self.m.core.pluginManager.getPlugin(self.pluginname) self.plugin = self.pluginclass(self) @lock -- cgit v1.2.3 From 17b3595dc5db8b3270e6bcd07176ed4b7b47930a Mon Sep 17 00:00:00 2001 From: RaNaN Date: Sun, 15 Jan 2012 19:28:12 +0100 Subject: improved handling of content-disposition --- module/PyFile.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) (limited to 'module/PyFile.py') diff --git a/module/PyFile.py b/module/PyFile.py index 0c4c20705..d70f852d1 100644 --- a/module/PyFile.py +++ b/module/PyFile.py @@ -49,7 +49,7 @@ class PyFile(object): """ Represents a file object at runtime """ - __slots__ = ("m", "id", "url", "name", "size", "_size", "status", "pluginname", "packageid", + __slots__ = ("m", "id", "url", "_name", "name", "size", "_size", "status", "pluginname", "packageid", "error", "order", "lock", "plugin", "waitUntil", "active", "abort", "statusname", "reconnected", "progress", "maxprogress", "pluginclass") @@ -58,7 +58,7 @@ class PyFile(object): self.id = int(id) self.url = url - self.name = name + self._name = name self.size = size self.status = status self.pluginname = pluginname @@ -89,7 +89,21 @@ class PyFile(object): # will convert all sizes to ints size = property(lambda self: self._size, setSize) - + + def getName(self): + try: + if self.plugin.req.name: + return self.plugin.req.name + else: + return self._name + except: + return self._name + + def setName(self, name): + self._name = name + + name = property(getName, setName) + def __repr__(self): return "" % (self.id, self.name, self.pluginname) -- cgit v1.2.3 From 056ff14eb0250cb0da4eae4b980e11525589cf65 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Sun, 22 Jan 2012 17:49:35 +0100 Subject: encoding fixes --- module/PyFile.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'module/PyFile.py') diff --git a/module/PyFile.py b/module/PyFile.py index d70f852d1..4f8b95124 100644 --- a/module/PyFile.py +++ b/module/PyFile.py @@ -100,6 +100,10 @@ class PyFile(object): return self._name def setName(self, name): + """ Only set unicode or utf8 strings as name """ + if type(name) == str: + name = name.decode("utf8") + self._name = name name = property(getName, setName) -- cgit v1.2.3 From 4df2b77fdf42046fe19bd371be7c7255986b5980 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Tue, 6 Mar 2012 13:36:39 +0100 Subject: renamed hooks to addons, new filemanager and database, many new api methods you will loose ALL your LINKS, webinterface will NOT work --- module/PyFile.py | 211 ++++++++++++++++++++++++++----------------------------- 1 file changed, 98 insertions(+), 113 deletions(-) (limited to 'module/PyFile.py') diff --git a/module/PyFile.py b/module/PyFile.py index 4f8b95124..5e6a3fae3 100644 --- a/module/PyFile.py +++ b/module/PyFile.py @@ -14,78 +14,101 @@ along with this program; if not, see . @author: RaNaN - @author: mkaay """ - from time import sleep, time from threading import RLock -from module.utils import formatSize, lock +from module.utils import format_size, format_time, lock + +from Api import FileInfo, DownloadInfo, DownloadStatus statusMap = { - "finished": 0, - "offline": 1, - "online": 2, - "queued": 3, - "skipped": 4, - "waiting": 5, - "temp. offline": 6, - "starting": 7, - "failed": 8, - "aborted": 9, - "decrypting": 10, - "custom": 11, - "downloading": 12, - "processing": 13, - "unknown": 14, -} - - -def setSize(self, value): - self._size = int(value) + "none": 0, + "offline": 1, + "online": 2, + "queued": 3, + "paused": 4, + "finished": 5, + "skipped": 6, + "failed": 7, + "starting": 8, + "waiting": 9, + "downloading": 10, + "temp. offline": 11, + "aborted": 12, + "decrypting": 13, + "processing": 14, + "custom": 15, + "unknown": 16, + } class PyFile(object): """ Represents a file object at runtime """ - __slots__ = ("m", "id", "url", "_name", "name", "size", "_size", "status", "pluginname", "packageid", - "error", "order", "lock", "plugin", "waitUntil", "active", "abort", "statusname", + __slots__ = ("m", "fid", "_name", "_size", "filestatus", "media", "added", "fileorder", + "url", "pluginname", "hash", "status", "error", "packageid", + "lock", "plugin", "waitUntil", "active", "abort", "statusname", "reconnected", "progress", "maxprogress", "pluginclass") - def __init__(self, manager, id, url, name, size, status, error, pluginname, package, order): + @staticmethod + def fromInfoData(m, info): + f = PyFile(m, info.fid, info.name, info.size, info.status, info.media, info.added, info.fileorder, + "", "", "", DownloadStatus.NA, "", info.package) + if info.download: + f.url = info.download.url + f.pluginname = info.download.plugin + f.hash = info.download.hash + f.status = info.download.status + f.error = info.download.error + + return f + + def __init__(self, manager, fid, name, size, filestatus, media, added, fileorder, + url, pluginname, hash, status, error, package): + self.m = manager - - self.id = int(id) - self.url = url + + self.fid = int(fid) self._name = name - self.size = size - self.status = status + self._size = size + self.filestatus = filestatus + self.media = media + self.added = added + self.fileorder = fileorder + self.url = url self.pluginname = pluginname - self.packageid = package #should not be used, use package() instead + self.hash = hash + self.status = status self.error = error - self.order = order + self.packageid = package #should not be used, use package() instead # database information ends here self.lock = RLock() - + self.plugin = None #self.download = None - + self.waitUntil = 0 # time() + time to wait - + # status attributes self.active = False #obsolete? self.abort = False self.reconnected = False self.statusname = None - + self.progress = 0 self.maxprogress = 100 - self.m.cache[int(id)] = self + @property + def id(self): + self.m.core.log.debug("Deprecated attr .id, use .fid instead") + return self.fid + def setSize(self, value): + self._size = int(value) # will convert all sizes to ints size = property(lambda self: self._size, setSize) @@ -120,19 +143,17 @@ class PyFile(object): @lock def hasPlugin(self): - """Thread safe way to determine this file has initialized plugin attribute - - :return: - """ + """Thread safe way to determine this file has initialized plugin attribute""" return hasattr(self, "plugin") and self.plugin - + def package(self): """ return package instance""" return self.m.getPackage(self.packageid) def setStatus(self, status): self.status = statusMap[status] - self.sync() #@TODO needed aslong no better job approving exists + # needs to sync so status is written to database + self.sync() def setCustomStatus(self, msg, status="processing"): self.statusname = msg @@ -143,60 +164,36 @@ class PyFile(object): return self.m.statusMsg[self.status] else: return self.statusname - + def hasStatus(self, status): return statusMap[status] == self.status - + def sync(self): """sync PyFile instance with database""" - self.m.updateLink(self) + self.m.updateFile(self) @lock def release(self): """sync and remove from cache""" - # file has valid package - if self.packageid > 0: - self.sync() - if hasattr(self, "plugin") and self.plugin: self.plugin.clean() del self.plugin - self.m.releaseLink(self.id) - - def delete(self): - """delete pyfile from database""" - self.m.deleteLink(self.id) - - def toDict(self): - """return dict with all information for interface""" - return self.toDbDict() - - def toDbDict(self): - """return data as dict for databse - - format: - - { - id: {'url': url, 'name': name ... } - } - - """ - return { - self.id: { - 'id': self.id, - 'url': self.url, - 'name': self.name, - 'plugin': self.pluginname, - 'size': self.getSize(), - 'format_size': self.formatSize(), - 'status': self.status, - 'statusmsg': self.getStatusName(), - 'package': self.packageid, - 'error': self.error, - 'order': self.order - } - } + self.m.releaseFile(self.fid) + + + def toInfoData(self): + return FileInfo(self.fid, self.getName(), self.packageid, self.getSize(), self.filestatus, + self.media, self.added, self.fileorder, DownloadInfo( + self.url, self.pluginname, self.hash, self.status, self.getStatusName(), self.error + ) + ) + + def getPath(self): + pass + + def move(self, pid): + pass def abortDownload(self): """abort pyfile if possible""" @@ -205,19 +202,19 @@ class PyFile(object): if self.plugin and self.plugin.req: self.plugin.req.abortDownloads() sleep(0.1) - + self.abort = False if self.hasPlugin() and self.plugin.req: self.plugin.req.abortDownloads() self.release() - + def finishIfDone(self): """set status to finish and release file if every thread is finished with it""" if self.id in self.m.core.threadManager.processingIds(): return False - + self.setStatus("finished") self.release() self.m.checkAllLinksFinished() @@ -225,62 +222,50 @@ class PyFile(object): def checkIfProcessed(self): self.m.checkAllLinksProcessed(self.id) - + def formatWait(self): """ formats and return wait time in humanreadable format """ - seconds = self.waitUntil - time() - - if seconds < 0: return "00:00:00" - - hours, seconds = divmod(seconds, 3600) - minutes, seconds = divmod(seconds, 60) - return "%.2i:%.2i:%.2i" % (hours, minutes, seconds) - + return format_time(self.waitUntil - time()) + def formatSize(self): """ formats size to readable format """ - return formatSize(self.getSize()) + return format_size(self.getSize()) def formatETA(self): """ formats eta to readable format """ - seconds = self.getETA() - - if seconds < 0: return "00:00:00" - - hours, seconds = divmod(seconds, 3600) - minutes, seconds = divmod(seconds, 60) - return "%.2i:%.2i:%.2i" % (hours, minutes, seconds) - + return format_time(self.getETA()) + def getSpeed(self): """ calculates speed """ try: return self.plugin.req.speed except: return 0 - + def getETA(self): """ gets established time of arrival""" try: return self.getBytesLeft() / self.getSpeed() except: return 0 - + def getBytesLeft(self): """ gets bytes left """ try: return self.plugin.req.size - self.plugin.req.arrived except: return 0 - + def getPercent(self): """ get % of download """ - if self.status == 12: + if self.status == DownloadStatus.Downloading: try: return self.plugin.req.percent except: return 0 else: return self.progress - + def getSize(self): """ get size of download """ try: @@ -290,7 +275,7 @@ class PyFile(object): return self.size except: return self.size - + def notifyChange(self): self.m.core.eventManager.dispatchEvent("linkUpdated", self.id, self.packageid) -- cgit v1.2.3 From 829244a6140763712d50ed046c33f415f2b04301 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Tue, 15 May 2012 19:22:34 +0200 Subject: some multiuser db changes --- module/PyFile.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) (limited to 'module/PyFile.py') diff --git a/module/PyFile.py b/module/PyFile.py index 5e6a3fae3..4cd0488a0 100644 --- a/module/PyFile.py +++ b/module/PyFile.py @@ -1,20 +1,19 @@ #!/usr/bin/env python -""" - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 3 of the License, - or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, see . - - @author: RaNaN -""" +# -*- coding: utf-8 -*- + +############################################################################### +# Copyright(c) 2008-2012 pyLoad Team +# http://www.pyload.org +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Subjected to the terms and conditions in LICENSE +# +# @author: RaNaN +############################################################################### from time import sleep, time from threading import RLock @@ -48,14 +47,14 @@ class PyFile(object): Represents a file object at runtime """ __slots__ = ("m", "fid", "_name", "_size", "filestatus", "media", "added", "fileorder", - "url", "pluginname", "hash", "status", "error", "packageid", + "url", "pluginname", "hash", "status", "error", "packageid", "ownerid", "lock", "plugin", "waitUntil", "active", "abort", "statusname", "reconnected", "progress", "maxprogress", "pluginclass") @staticmethod def fromInfoData(m, info): f = PyFile(m, info.fid, info.name, info.size, info.status, info.media, info.added, info.fileorder, - "", "", "", DownloadStatus.NA, "", info.package) + "", "", "", DownloadStatus.NA, "", info.package, info.owner) if info.download: f.url = info.download.url f.pluginname = info.download.plugin @@ -66,7 +65,7 @@ class PyFile(object): return f def __init__(self, manager, fid, name, size, filestatus, media, added, fileorder, - url, pluginname, hash, status, error, package): + url, pluginname, hash, status, error, package, owner): self.m = manager @@ -82,6 +81,7 @@ class PyFile(object): self.hash = hash self.status = status self.error = error + self.ownerid = owner self.packageid = package #should not be used, use package() instead # database information ends here @@ -183,7 +183,7 @@ class PyFile(object): def toInfoData(self): - return FileInfo(self.fid, self.getName(), self.packageid, self.getSize(), self.filestatus, + return FileInfo(self.fid, self.getName(), self.packageid, self.ownerid, self.getSize(), self.filestatus, self.media, self.added, self.fileorder, DownloadInfo( self.url, self.pluginname, self.hash, self.status, self.getStatusName(), self.error ) -- cgit v1.2.3 From 0d2d6daef850ac6bcc7fafccd230e52d2a862c2c Mon Sep 17 00:00:00 2001 From: RaNaN Date: Sun, 3 Jun 2012 17:45:10 +0200 Subject: updates for database + api --- module/PyFile.py | 285 ------------------------------------------------------- 1 file changed, 285 deletions(-) delete mode 100644 module/PyFile.py (limited to 'module/PyFile.py') diff --git a/module/PyFile.py b/module/PyFile.py deleted file mode 100644 index 4cd0488a0..000000000 --- a/module/PyFile.py +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -############################################################################### -# Copyright(c) 2008-2012 pyLoad Team -# http://www.pyload.org -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# Subjected to the terms and conditions in LICENSE -# -# @author: RaNaN -############################################################################### - -from time import sleep, time -from threading import RLock - -from module.utils import format_size, format_time, lock - -from Api import FileInfo, DownloadInfo, DownloadStatus - -statusMap = { - "none": 0, - "offline": 1, - "online": 2, - "queued": 3, - "paused": 4, - "finished": 5, - "skipped": 6, - "failed": 7, - "starting": 8, - "waiting": 9, - "downloading": 10, - "temp. offline": 11, - "aborted": 12, - "decrypting": 13, - "processing": 14, - "custom": 15, - "unknown": 16, - } - -class PyFile(object): - """ - Represents a file object at runtime - """ - __slots__ = ("m", "fid", "_name", "_size", "filestatus", "media", "added", "fileorder", - "url", "pluginname", "hash", "status", "error", "packageid", "ownerid", - "lock", "plugin", "waitUntil", "active", "abort", "statusname", - "reconnected", "progress", "maxprogress", "pluginclass") - - @staticmethod - def fromInfoData(m, info): - f = PyFile(m, info.fid, info.name, info.size, info.status, info.media, info.added, info.fileorder, - "", "", "", DownloadStatus.NA, "", info.package, info.owner) - if info.download: - f.url = info.download.url - f.pluginname = info.download.plugin - f.hash = info.download.hash - f.status = info.download.status - f.error = info.download.error - - return f - - def __init__(self, manager, fid, name, size, filestatus, media, added, fileorder, - url, pluginname, hash, status, error, package, owner): - - self.m = manager - - self.fid = int(fid) - self._name = name - self._size = size - self.filestatus = filestatus - self.media = media - self.added = added - self.fileorder = fileorder - self.url = url - self.pluginname = pluginname - self.hash = hash - self.status = status - self.error = error - self.ownerid = owner - self.packageid = package #should not be used, use package() instead - # database information ends here - - self.lock = RLock() - - self.plugin = None - #self.download = None - - self.waitUntil = 0 # time() + time to wait - - # status attributes - self.active = False #obsolete? - self.abort = False - self.reconnected = False - - self.statusname = None - - self.progress = 0 - self.maxprogress = 100 - - @property - def id(self): - self.m.core.log.debug("Deprecated attr .id, use .fid instead") - return self.fid - - def setSize(self, value): - self._size = int(value) - - # will convert all sizes to ints - size = property(lambda self: self._size, setSize) - - def getName(self): - try: - if self.plugin.req.name: - return self.plugin.req.name - else: - return self._name - except: - return self._name - - def setName(self, name): - """ Only set unicode or utf8 strings as name """ - if type(name) == str: - name = name.decode("utf8") - - self._name = name - - name = property(getName, setName) - - def __repr__(self): - return "" % (self.id, self.name, self.pluginname) - - @lock - def initPlugin(self): - """ inits plugin instance """ - if not self.plugin: - self.pluginclass = self.m.core.pluginManager.getPlugin(self.pluginname) - self.plugin = self.pluginclass(self) - - @lock - def hasPlugin(self): - """Thread safe way to determine this file has initialized plugin attribute""" - return hasattr(self, "plugin") and self.plugin - - def package(self): - """ return package instance""" - return self.m.getPackage(self.packageid) - - def setStatus(self, status): - self.status = statusMap[status] - # needs to sync so status is written to database - self.sync() - - def setCustomStatus(self, msg, status="processing"): - self.statusname = msg - self.setStatus(status) - - def getStatusName(self): - if self.status not in (13, 14) or not self.statusname: - return self.m.statusMsg[self.status] - else: - return self.statusname - - def hasStatus(self, status): - return statusMap[status] == self.status - - def sync(self): - """sync PyFile instance with database""" - self.m.updateFile(self) - - @lock - def release(self): - """sync and remove from cache""" - if hasattr(self, "plugin") and self.plugin: - self.plugin.clean() - del self.plugin - - self.m.releaseFile(self.fid) - - - def toInfoData(self): - return FileInfo(self.fid, self.getName(), self.packageid, self.ownerid, self.getSize(), self.filestatus, - self.media, self.added, self.fileorder, DownloadInfo( - self.url, self.pluginname, self.hash, self.status, self.getStatusName(), self.error - ) - ) - - def getPath(self): - pass - - def move(self, pid): - pass - - def abortDownload(self): - """abort pyfile if possible""" - while self.id in self.m.core.threadManager.processingIds(): - self.abort = True - if self.plugin and self.plugin.req: - self.plugin.req.abortDownloads() - sleep(0.1) - - self.abort = False - if self.hasPlugin() and self.plugin.req: - self.plugin.req.abortDownloads() - - self.release() - - def finishIfDone(self): - """set status to finish and release file if every thread is finished with it""" - - if self.id in self.m.core.threadManager.processingIds(): - return False - - self.setStatus("finished") - self.release() - self.m.checkAllLinksFinished() - return True - - def checkIfProcessed(self): - self.m.checkAllLinksProcessed(self.id) - - def formatWait(self): - """ formats and return wait time in humanreadable format """ - return format_time(self.waitUntil - time()) - - def formatSize(self): - """ formats size to readable format """ - return format_size(self.getSize()) - - def formatETA(self): - """ formats eta to readable format """ - return format_time(self.getETA()) - - def getSpeed(self): - """ calculates speed """ - try: - return self.plugin.req.speed - except: - return 0 - - def getETA(self): - """ gets established time of arrival""" - try: - return self.getBytesLeft() / self.getSpeed() - except: - return 0 - - def getBytesLeft(self): - """ gets bytes left """ - try: - return self.plugin.req.size - self.plugin.req.arrived - except: - return 0 - - def getPercent(self): - """ get % of download """ - if self.status == DownloadStatus.Downloading: - try: - return self.plugin.req.percent - except: - return 0 - else: - return self.progress - - def getSize(self): - """ get size of download """ - try: - if self.plugin.req.size: - return self.plugin.req.size - else: - return self.size - except: - return self.size - - def notifyChange(self): - self.m.core.eventManager.dispatchEvent("linkUpdated", self.id, self.packageid) - - def setProgress(self, value): - if not value == self.progress: - self.progress = value - self.notifyChange() -- cgit v1.2.3