PageRenderTime 55ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/sabnzbd/bpsmeter.py

https://github.com/janschejbal/sabnzbd
Python | 369 lines | 306 code | 31 blank | 32 comment | 20 complexity | 13ad85eb5c8aba8a3dfe6b76a4b3add2 MD5 | raw file
  1. #!/usr/bin/python -OO
  2. # Copyright 2008-2011 The SABnzbd-Team <team@sabnzbd.org>
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; either version 2
  7. # of the License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  17. """
  18. sabnzbd.bpsmeter - bpsmeter
  19. """
  20. import time
  21. import logging
  22. import re
  23. import sabnzbd
  24. from sabnzbd.constants import BYTES_FILE_NAME
  25. import sabnzbd.cfg as cfg
  26. DAY = float(24*60*60)
  27. WEEK = DAY * 7
  28. #------------------------------------------------------------------------------
  29. def tomorrow(t):
  30. """ Return timestamp for tomorrow (midnight) """
  31. now = time.localtime(t)
  32. ntime = (now[0], now[1], now[2], 0, 0, 0, now[6], now[7], now[8])
  33. return time.mktime(ntime) + DAY
  34. def this_week(t):
  35. """ Return timestamp for start of this week (monday) """
  36. while 1:
  37. tm = time.localtime(t)
  38. if tm.tm_wday == 0:
  39. break
  40. t -= DAY
  41. monday = (tm.tm_year, tm.tm_mon, tm.tm_mday, 0, 0, 0, 0, 0, tm.tm_isdst)
  42. return time.mktime(monday)
  43. def next_week(t):
  44. """ Return timestamp for start of next week (monday) """
  45. return this_week(t) + WEEK
  46. def this_month(t):
  47. """ Return timestamp for start of next month """
  48. now = time.localtime(t)
  49. ntime = (now[0], now[1], 1, 0, 0, 0, 0, 0, now[8])
  50. return time.mktime(ntime)
  51. _DAYS = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
  52. def last_month_day(t=None):
  53. """ Return last day of this month """
  54. t = t or time.localtime(t)
  55. year, month = time.localtime(t)[:2]
  56. day = _DAYS[month]
  57. if day == 28 and (year % 4) == 0 and (year % 400) == 0:
  58. day = 29
  59. return day
  60. def this_month_day(t=None):
  61. """ Return current day of the week, month 1..31 """
  62. t = t or time.localtime(t)
  63. return time.localtime(t).tm_mday
  64. def this_week_day(t=None):
  65. """ Return current day of the week 1..7 """
  66. t = t or time.localtime(t)
  67. return time.localtime(t).tm_wday + 1
  68. def next_month(t):
  69. """ Return timestamp for start of next month """
  70. now = time.localtime(t)
  71. month = now.tm_mon + 1
  72. year = now.tm_year
  73. if month > 12:
  74. month = 1
  75. year += 1
  76. ntime = (year, month, 1, 0, 0, 0, 0, 0, now[8])
  77. return time.mktime(ntime)
  78. class BPSMeter(object):
  79. do = None
  80. def __init__(self):
  81. t = time.time()
  82. self.start_time = t
  83. self.log_time = t
  84. self.last_update = t
  85. self.bps = 0.0
  86. self.day_total = {}
  87. self.week_total = {}
  88. self.month_total = {}
  89. self.grand_total = {}
  90. self.end_of_day = tomorrow(t) # Time that current day will end
  91. self.end_of_week = next_week(t) # Time that current day will end
  92. self.end_of_month = next_month(t) # Time that current month will end
  93. self.q_day = 1 # Day of quota reset
  94. self.q_period = 'm' # Daily/Weekly/Monthly quota = d/w/m
  95. self.quota = self.left = 0.0 # Quota and remaining quota
  96. self.have_quota = False # Flag for quota active
  97. self.q_time = 0L # Next reset time for quota
  98. self.q_hour = 0 # Quota reset hour
  99. self.q_minute = 0 # Quota reset minute
  100. BPSMeter.do = self
  101. def save(self):
  102. """ Save admin to disk """
  103. if self.grand_total or self.day_total or self.week_total or self.month_total:
  104. data = (self.last_update, self.grand_total,
  105. self.day_total, self.week_total, self.month_total,
  106. self.end_of_day, self.end_of_week, self.end_of_month,
  107. self.quota, self.left, self.q_time
  108. )
  109. sabnzbd.save_admin(data, BYTES_FILE_NAME)
  110. def read(self):
  111. """ Read admin from disk """
  112. quota = self.left = cfg.quota_size.get_float() # Quota for this period
  113. data = sabnzbd.load_admin(BYTES_FILE_NAME)
  114. try:
  115. self.last_update, self.grand_total, \
  116. self.day_total, self.week_total, self.month_total, \
  117. self.end_of_day, self.end_of_week, self.end_of_month = data[:8]
  118. if len(data) == 11:
  119. self.quota, self.left, self.q_time = data[8:]
  120. logging.debug('Read quota q=%s l=%s reset=%s',
  121. self.quota, self.left, self.q_time)
  122. if abs(quota - self.quota) > 0.5:
  123. self.change_quota()
  124. else:
  125. self.quota = self.left = cfg.quota_size.get_float()
  126. self.have_quota = bool(cfg.quota_size())
  127. res = self.reset_quota()
  128. except:
  129. # Get the latest data from the database and assign to a fake server
  130. logging.debug('Setting default BPS meter values')
  131. grand, month, week = sabnzbd.proxy_get_history_size()
  132. if grand: self.grand_total['x'] = grand
  133. if month: self.month_total['x'] = month
  134. if week: self.week_total['x'] = week
  135. res = False
  136. # Force update of counters
  137. self.update()
  138. return res
  139. def update(self, server=None, amount=0, testtime=None):
  140. """ Update counters for "server" with "amount" bytes
  141. """
  142. if testtime:
  143. t = testtime
  144. else:
  145. t = time.time()
  146. if t > self.end_of_day:
  147. # current day passed. get new end of day
  148. self.day_total = {}
  149. self.end_of_day = tomorrow(t) - 1.0
  150. if t > self.end_of_week:
  151. self.week_total = {}
  152. self.end_of_week = next_week(t) - 1.0
  153. if t > self.end_of_month:
  154. self.month_total = {}
  155. self.end_of_month = next_month(t) - 1.0
  156. if server:
  157. if server not in self.day_total:
  158. self.day_total[server] = 0L
  159. self.day_total[server] += amount
  160. if server not in self.week_total:
  161. self.week_total[server] = 0L
  162. self.week_total[server] += amount
  163. if server not in self.month_total:
  164. self.month_total[server] = 0L
  165. self.month_total[server] += amount
  166. if server not in self.grand_total:
  167. self.grand_total[server] = 0L
  168. self.grand_total[server] += amount
  169. # Quota check
  170. if self.have_quota:
  171. self.left -= amount
  172. if self.left <= 0.0:
  173. from sabnzbd.downloader import Downloader
  174. if Downloader.do and not Downloader.do.paused:
  175. Downloader.do.pause()
  176. logging.warning(Ta('Quota spent, pausing downloading'))
  177. # Speedometer
  178. try:
  179. self.bps = (self.bps * (self.last_update - self.start_time)
  180. + amount) / (t - self.start_time)
  181. except:
  182. self.bps = 0.0
  183. self.last_update = t
  184. check_time = t - 5.0
  185. if self.start_time < check_time:
  186. self.start_time = check_time
  187. if self.bps < 0.01:
  188. self.reset()
  189. elif self.log_time < check_time:
  190. logging.debug("bps: %s", self.bps)
  191. self.log_time = t
  192. def reset(self):
  193. t = time.time()
  194. self.start_time = t
  195. self.log_time = t
  196. self.last_update = t
  197. self.bps = 0.0
  198. def get_sums(self):
  199. """ return tuple of grand, month, week, day totals """
  200. return (sum([v for v in self.grand_total.values()]),
  201. sum([v for v in self.month_total.values()]),
  202. sum([v for v in self.week_total.values()]),
  203. sum([v for v in self.day_total.values()])
  204. )
  205. def amounts(self, server):
  206. """ Return grand, month, week, day totals for specified server """
  207. return self.grand_total.get(server, 0L), \
  208. self.month_total.get(server, 0L), \
  209. self.week_total.get(server, 0L), \
  210. self.day_total.get(server, 0L)
  211. def get_bps(self):
  212. return self.bps
  213. def reset_quota(self, force=False):
  214. """ Check if it's time to reset the quota, optionally resuming
  215. Return True, when still paused
  216. """
  217. if force or (self.have_quota and time.time() > (self.q_time - 50)):
  218. self.quota = self.left = cfg.quota_size.get_float()
  219. logging.info('Quota was reset to %s', self.quota)
  220. if cfg.quota_resume():
  221. logging.info('Auto-resume due to quota reset')
  222. if sabnzbd.downloader.Downloader.do:
  223. sabnzbd.downloader.Downloader.do.resume()
  224. self.next_reset()
  225. return False
  226. else:
  227. return True
  228. def next_reset(self, t=None):
  229. """ Determine next reset time
  230. """
  231. t = t or time.time()
  232. tm = time.localtime(t)
  233. if self.q_period == 'd':
  234. nx = (tm[0], tm[1], tm[2], self.q_hour, self.q_minute, 0, 0, 0, tm[8])
  235. if (tm.tm_hour + tm.tm_min * 60) >= (self.q_hour + self.q_minute * 60):
  236. # If today's moment has passed, it will happen tomorrow
  237. t = time.mktime(nx) + 24 * 3600
  238. tm = time.localtime(t)
  239. elif self.q_period == 'w':
  240. if self.q_day < tm.tm_wday+1 or (self.q_day == tm.tm_wday+1 and (tm.tm_hour + tm.tm_min * 60) >= (self.q_hour + self.q_minute * 60)):
  241. tm = time.localtime(next_week(t))
  242. dif = abs(self.q_day - tm.tm_wday - 1)
  243. t = time.mktime(tm) + dif * 24 * 3600
  244. tm = time.localtime(t)
  245. elif self.q_period == 'm':
  246. if self.q_day < tm.tm_mday or (self.q_day == tm.tm_mday and (tm.tm_hour + tm.tm_min * 60) >= (self.q_hour + self.q_minute * 60)):
  247. tm = time.localtime(next_month(t))
  248. tm = (tm[0], tm[1], self.q_day, self.q_hour, self.q_minute, 0, 0, 0, tm[8])
  249. else:
  250. return
  251. tm = (tm[0], tm[1], tm[2], self.q_hour, self.q_minute, 0, 0, 0, tm[8])
  252. self.q_time = time.mktime(tm)
  253. logging.debug('Will reset quota at %s', tm)
  254. def change_quota(self, allow_resume=True):
  255. """ Update quota, potentially pausing downloader
  256. """
  257. if not self.have_quota and self.quota < 0.5:
  258. # Never set, use last period's size
  259. per = cfg.quota_period()
  260. sums = self.get_sums()
  261. if per == 'd':
  262. self.left = sums[3]
  263. elif per == 'w':
  264. self.left = sums[2]
  265. elif per == 'm':
  266. self.left = sums[1]
  267. self.have_quota = bool(cfg.quota_size())
  268. if self.have_quota:
  269. quota = cfg.quota_size.get_float()
  270. self.left = quota - (self.quota - self.left)
  271. self.quota = quota
  272. else:
  273. self.quota = self.left = 0L
  274. self.update(0)
  275. self.next_reset()
  276. if self.left > 0.5:
  277. from sabnzbd.downloader import Downloader
  278. if allow_resume and cfg.quota_resume() and Downloader.do and Downloader.do.paused:
  279. Downloader.do.resume()
  280. # Pattern = <day#> <hh:mm>
  281. # The <day> and <hh:mm> part can both be optional
  282. __re_day = re.compile('^\s*(\d+)[^:]*')
  283. __re_hm = re.compile('(\d+):(\d+)\s*$')
  284. def get_quota(self):
  285. """ If quota active, return check-function, hour, minute
  286. """
  287. if self.have_quota:
  288. self.q_period = cfg.quota_period()[0].lower()
  289. self.q_day = 1
  290. self.q_hour = self.q_minute = 0
  291. txt = cfg.quota_day().lower()
  292. m = self.__re_day.search(txt)
  293. if m:
  294. self.q_day = int(m.group(1))
  295. m = self.__re_hm.search(txt)
  296. if m:
  297. self.q_hour = int(m.group(1))
  298. self.q_minute = int(m.group(2))
  299. self.q_day = max(1, self.q_day)
  300. self.q_day = min(7, self.q_day)
  301. self.change_quota(allow_resume=False)
  302. return quota_handler, self.q_hour, self.q_minute
  303. else:
  304. return None, 0, 0
  305. def quota_handler():
  306. """ To be called from scheduler """
  307. logging.debug('Checking quota')
  308. BPSMeter.do.reset_quota()
  309. BPSMeter()