PageRenderTime 26ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/sickbeard/versionChecker.py

https://github.com/bbucommander/Sick-Beard
Python | 486 lines | 427 code | 23 blank | 36 comment | 20 complexity | c462ccc7c69b15d55fc50af1e0986b74 MD5 | raw file
  1. # Author: Nic Wolfe <nic@wolfeden.ca>
  2. # URL: http://code.google.com/p/sickbeard/
  3. #
  4. # This file is part of Sick Beard.
  5. #
  6. # Sick Beard is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # Sick Beard is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>.
  18. import sickbeard
  19. from sickbeard import version, ui
  20. from sickbeard import logger
  21. from sickbeard import scene_exceptions
  22. from sickbeard.exceptions import ex
  23. import os, platform, shutil
  24. import subprocess, re
  25. import urllib, urllib2
  26. import zipfile, tarfile
  27. from urllib2 import URLError
  28. from lib.pygithub import github
  29. class CheckVersion():
  30. """
  31. Version check class meant to run as a thread object with the SB scheduler.
  32. """
  33. def __init__(self):
  34. self.install_type = self.find_install_type()
  35. if self.install_type == 'win':
  36. self.updater = WindowsUpdateManager()
  37. elif self.install_type == 'git':
  38. self.updater = GitUpdateManager()
  39. elif self.install_type == 'source':
  40. self.updater = SourceUpdateManager()
  41. else:
  42. self.updater = None
  43. def run(self):
  44. self.check_for_new_version()
  45. # refresh scene exceptions too
  46. scene_exceptions.retrieve_exceptions()
  47. def find_install_type(self):
  48. """
  49. Determines how this copy of SB was installed.
  50. returns: type of installation. Possible values are:
  51. 'win': any compiled windows build
  52. 'git': running from source using git
  53. 'source': running from source without git
  54. """
  55. # check if we're a windows build
  56. if version.SICKBEARD_VERSION.startswith('build '):
  57. install_type = 'win'
  58. elif os.path.isdir(os.path.join(sickbeard.PROG_DIR, '.git')):
  59. install_type = 'git'
  60. else:
  61. install_type = 'source'
  62. return install_type
  63. def check_for_new_version(self, force=False):
  64. """
  65. Checks the internet for a newer version.
  66. returns: bool, True for new version or False for no new version.
  67. force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced
  68. """
  69. if not sickbeard.VERSION_NOTIFY and not force:
  70. logger.log(u"Version checking is disabled, not checking for the newest version")
  71. return False
  72. logger.log(u"Checking if "+self.install_type+" needs an update")
  73. if not self.updater.need_update():
  74. logger.log(u"No update needed")
  75. if force:
  76. ui.notifications.message('No update needed')
  77. return False
  78. self.updater.set_newest_text()
  79. return True
  80. def update(self):
  81. if self.updater.need_update():
  82. return self.updater.update()
  83. class UpdateManager():
  84. def get_update_url(self):
  85. return sickbeard.WEB_ROOT+"/home/update/?pid="+str(sickbeard.PID)
  86. class WindowsUpdateManager(UpdateManager):
  87. def __init__(self):
  88. self._cur_version = None
  89. self._newest_version = None
  90. self.gc_url = 'http://code.google.com/p/sickbeard/downloads/list'
  91. def _find_installed_version(self):
  92. return int(sickbeard.version.SICKBEARD_VERSION[6:])
  93. def _find_newest_version(self, whole_link=False):
  94. """
  95. Checks google code for the newest Windows binary build. Returns either the
  96. build number or the entire build URL depending on whole_link's value.
  97. whole_link: If True, returns the entire URL to the release. If False, it returns
  98. only the build number. default: False
  99. """
  100. regex = "http://sickbeard.googlecode.com/files/SickBeard\-win32\-alpha\-build(\d+)(?:\.\d+)?\.zip"
  101. svnFile = urllib.urlopen(self.gc_url)
  102. for curLine in svnFile.readlines():
  103. match = re.search(regex, curLine)
  104. if match:
  105. if whole_link:
  106. return match.group(0)
  107. else:
  108. return int(match.group(1))
  109. return None
  110. def need_update(self):
  111. self._cur_version = self._find_installed_version()
  112. self._newest_version = self._find_newest_version()
  113. if self._newest_version > self._cur_version:
  114. return True
  115. def set_newest_text(self):
  116. new_str = 'There is a <a href="'+self.gc_url+'" onclick="window.open(this.href); return false;">newer version available</a> (build '+str(self._newest_version)+')'
  117. new_str += "&mdash; <a href=\""+self.get_update_url()+"\">Update Now</a>"
  118. sickbeard.NEWEST_VERSION_STRING = new_str
  119. def update(self):
  120. new_link = self._find_newest_version(True)
  121. if not new_link:
  122. logger.log(u"Unable to find a new version link on google code, not updating")
  123. return False
  124. # download the zip
  125. try:
  126. logger.log(u"Downloading update file from "+str(new_link))
  127. (filename, headers) = urllib.urlretrieve(new_link) #@UnusedVariable
  128. # prepare the update dir
  129. sb_update_dir = os.path.join(sickbeard.PROG_DIR, 'sb-update')
  130. logger.log(u"Clearing out update folder "+sb_update_dir+" before unzipping")
  131. if os.path.isdir(sb_update_dir):
  132. shutil.rmtree(sb_update_dir)
  133. # unzip it to sb-update
  134. logger.log(u"Unzipping from "+str(filename)+" to "+sb_update_dir)
  135. update_zip = zipfile.ZipFile(filename, 'r')
  136. update_zip.extractall(sb_update_dir)
  137. update_zip.close()
  138. # find update dir name
  139. update_dir_contents = os.listdir(sb_update_dir)
  140. if len(update_dir_contents) != 1:
  141. logger.log("Invalid update data, update failed. Maybe try deleting your sb-update folder?", logger.ERROR)
  142. return False
  143. content_dir = os.path.join(sb_update_dir, update_dir_contents[0])
  144. old_update_path = os.path.join(content_dir, 'updater.exe')
  145. new_update_path = os.path.join(sickbeard.PROG_DIR, 'updater.exe')
  146. logger.log(u"Copying new update.exe file from "+old_update_path+" to "+new_update_path)
  147. shutil.move(old_update_path, new_update_path)
  148. # delete the zip
  149. logger.log(u"Deleting zip file from "+str(filename))
  150. os.remove(filename)
  151. except Exception, e:
  152. logger.log(u"Error while trying to update: "+ex(e), logger.ERROR)
  153. return False
  154. return True
  155. class GitUpdateManager(UpdateManager):
  156. def __init__(self):
  157. self._cur_commit_hash = None
  158. self._newest_commit_hash = None
  159. self._num_commits_behind = 0
  160. self.git_url = 'http://code.google.com/p/sickbeard/downloads/list'
  161. def _git_error(self):
  162. error_message = 'Unable to find your git executable - either delete your .git folder and run from source OR <a href="http://code.google.com/p/sickbeard/wiki/AdvancedSettings" onclick="window.open(this.href); return false;">set git_path in your config.ini</a> to enable updates.'
  163. sickbeard.NEWEST_VERSION_STRING = error_message
  164. return None
  165. def _run_git(self, args):
  166. if sickbeard.GIT_PATH:
  167. git_locations = ['"'+sickbeard.GIT_PATH+'"']
  168. else:
  169. git_locations = ['git']
  170. # osx people who start SB from launchd have a broken path, so try a hail-mary attempt for them
  171. if platform.system().lower() == 'darwin':
  172. git_locations.append('/usr/local/git/bin/git')
  173. output = err = None
  174. for cur_git in git_locations:
  175. cmd = cur_git+' '+args
  176. try:
  177. logger.log(u"Executing "+cmd+" with your shell in "+sickbeard.PROG_DIR, logger.DEBUG)
  178. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=sickbeard.PROG_DIR)
  179. output, err = p.communicate()
  180. logger.log(u"git output: "+output, logger.DEBUG)
  181. except OSError:
  182. logger.log(u"Command "+cmd+" didn't work, couldn't find git.")
  183. continue
  184. if 'not found' in output or "not recognized as an internal or external command" in output:
  185. logger.log(u"Unable to find git with command "+cmd, logger.DEBUG)
  186. output = None
  187. elif 'fatal:' in output or err:
  188. logger.log(u"Git returned bad info, are you sure this is a git installation?", logger.ERROR)
  189. output = None
  190. elif output:
  191. break
  192. return (output, err)
  193. def _find_installed_version(self):
  194. """
  195. Attempts to find the currently installed version of Sick Beard.
  196. Uses git show to get commit version.
  197. Returns: True for success or False for failure
  198. """
  199. output, err = self._run_git('rev-parse HEAD') #@UnusedVariable
  200. if not output:
  201. return self._git_error()
  202. logger.log(u"Git output: "+str(output), logger.DEBUG)
  203. cur_commit_hash = output.strip()
  204. if not re.match('^[a-z0-9]+$', cur_commit_hash):
  205. logger.log(u"Output doesn't look like a hash, not using it", logger.ERROR)
  206. return self._git_error()
  207. self._cur_commit_hash = cur_commit_hash
  208. return True
  209. def _check_github_for_update(self):
  210. """
  211. Uses pygithub to ask github if there is a newer version that the provided
  212. commit hash. If there is a newer version it sets Sick Beard's version text.
  213. commit_hash: hash that we're checking against
  214. """
  215. self._num_commits_behind = 0
  216. self._newest_commit_hash = None
  217. gh = github.GitHub()
  218. # find newest commit
  219. for curCommit in gh.commits.forBranch('midgetspy', 'Sick-Beard', version.SICKBEARD_VERSION):
  220. if not self._newest_commit_hash:
  221. self._newest_commit_hash = curCommit.id
  222. if not self._cur_commit_hash:
  223. break
  224. if curCommit.id == self._cur_commit_hash:
  225. break
  226. self._num_commits_behind += 1
  227. logger.log(u"newest: "+str(self._newest_commit_hash)+" and current: "+str(self._cur_commit_hash)+" and num_commits: "+str(self._num_commits_behind), logger.DEBUG)
  228. def set_newest_text(self):
  229. # if we're up to date then don't set this
  230. if self._num_commits_behind == 35:
  231. message = "or else you're ahead of master"
  232. elif self._num_commits_behind > 0:
  233. message = "you're "+str(self._num_commits_behind)+' commits behind'
  234. else:
  235. return
  236. if self._newest_commit_hash:
  237. url = 'http://github.com/midgetspy/Sick-Beard/compare/'+self._cur_commit_hash+'...'+self._newest_commit_hash
  238. else:
  239. url = 'http://github.com/midgetspy/Sick-Beard/commits/'
  240. new_str = 'There is a <a href="'+url+'" onclick="window.open(this.href); return false;">newer version available</a> ('+message+')'
  241. new_str += "&mdash; <a href=\""+self.get_update_url()+"\">Update Now</a>"
  242. sickbeard.NEWEST_VERSION_STRING = new_str
  243. def need_update(self):
  244. self._find_installed_version()
  245. try:
  246. self._check_github_for_update()
  247. except Exception:
  248. logger.log(u"Unable to contact github, can't check for update", logger.ERROR)
  249. return False
  250. logger.log(u"After checking, cur_commit = "+str(self._cur_commit_hash)+", newest_commit = "+str(self._newest_commit_hash)+", num_commits_behind = "+str(self._num_commits_behind), logger.DEBUG)
  251. if self._num_commits_behind > 0:
  252. return True
  253. return False
  254. def update(self):
  255. """
  256. Calls git pull origin <branch> in order to update Sick Beard. Returns a bool depending
  257. on the call's success.
  258. """
  259. output, err = self._run_git('pull origin '+sickbeard.version.SICKBEARD_VERSION) #@UnusedVariable
  260. if not output:
  261. return self._git_error()
  262. pull_regex = '(\d+) files? changed, (\d+) insertions?\(\+\), (\d+) deletions?\(\-\)'
  263. (files, insertions, deletions) = (None, None, None)
  264. for line in output.split('\n'):
  265. if 'Already up-to-date.' in line:
  266. logger.log(u"No update available, not updating")
  267. logger.log(u"Output: "+str(output))
  268. return False
  269. elif line.endswith('Aborting.'):
  270. logger.log(u"Unable to update from git: "+line, logger.ERROR)
  271. logger.log(u"Output: "+str(output))
  272. return False
  273. match = re.search(pull_regex, line)
  274. if match:
  275. (files, insertions, deletions) = match.groups()
  276. break
  277. if None in (files, insertions, deletions):
  278. logger.log(u"Didn't find indication of success in output, assuming git pull failed", logger.ERROR)
  279. logger.log(u"Output: "+str(output))
  280. return False
  281. return True
  282. class SourceUpdateManager(GitUpdateManager):
  283. def _find_installed_version(self):
  284. version_file = os.path.join(sickbeard.PROG_DIR, 'version.txt')
  285. if not os.path.isfile(version_file):
  286. self._cur_commit_hash = None
  287. return
  288. fp = open(version_file, 'r')
  289. self._cur_commit_hash = fp.read().strip(' \n\r')
  290. fp.close()
  291. if not self._cur_commit_hash:
  292. self._cur_commit_hash = None
  293. def need_update(self):
  294. parent_result = GitUpdateManager.need_update(self)
  295. if not self._cur_commit_hash:
  296. return True
  297. else:
  298. return parent_result
  299. def set_newest_text(self):
  300. if not self._cur_commit_hash:
  301. logger.log(u"Unknown current version, don't know if we should update or not", logger.DEBUG)
  302. new_str = "Unknown version: If you've never used the Sick Beard upgrade system then I don't know what version you have."
  303. new_str += "&mdash; <a href=\""+self.get_update_url()+"\">Update Now</a>"
  304. sickbeard.NEWEST_VERSION_STRING = new_str
  305. else:
  306. GitUpdateManager.set_newest_text(self)
  307. def update(self):
  308. """
  309. Downloads the latest source tarball from github and installs it over the existing version.
  310. """
  311. tar_download_url = 'http://github.com/midgetspy/Sick-Beard/tarball/'+version.SICKBEARD_VERSION
  312. sb_update_dir = os.path.join(sickbeard.PROG_DIR, 'sb-update')
  313. version_path = os.path.join(sickbeard.PROG_DIR, 'version.txt')
  314. # retrieve file
  315. try:
  316. logger.log(u"Downloading update from "+tar_download_url)
  317. data = urllib2.urlopen(tar_download_url)
  318. except (IOError, URLError):
  319. logger.log(u"Unable to retrieve new version from "+tar_download_url+", can't update", logger.ERROR)
  320. return False
  321. download_name = data.geturl().split('/')[-1]
  322. tar_download_path = os.path.join(sickbeard.PROG_DIR, download_name)
  323. # save to disk
  324. f = open(tar_download_path, 'wb')
  325. f.write(data.read())
  326. f.close()
  327. # extract to temp folder
  328. logger.log(u"Extracting file "+tar_download_path)
  329. tar = tarfile.open(tar_download_path)
  330. tar.extractall(sb_update_dir)
  331. tar.close()
  332. # delete .tar.gz
  333. logger.log(u"Deleting file "+tar_download_path)
  334. os.remove(tar_download_path)
  335. # find update dir name
  336. update_dir_contents = [x for x in os.listdir(sb_update_dir) if os.path.isdir(os.path.join(sb_update_dir, x))]
  337. if len(update_dir_contents) != 1:
  338. logger.log(u"Invalid update data, update failed: "+str(update_dir_contents), logger.ERROR)
  339. return False
  340. content_dir = os.path.join(sb_update_dir, update_dir_contents[0])
  341. # walk temp folder and move files to main folder
  342. for dirname, dirnames, filenames in os.walk(content_dir): #@UnusedVariable
  343. dirname = dirname[len(content_dir)+1:]
  344. for curfile in filenames:
  345. old_path = os.path.join(content_dir, dirname, curfile)
  346. new_path = os.path.join(sickbeard.PROG_DIR, dirname, curfile)
  347. if os.path.isfile(new_path):
  348. os.remove(new_path)
  349. os.renames(old_path, new_path)
  350. # update version.txt with commit hash
  351. try:
  352. ver_file = open(version_path, 'w')
  353. ver_file.write(self._newest_commit_hash)
  354. ver_file.close()
  355. except IOError, e:
  356. logger.log(u"Unable to write version file, update not complete: "+ex(e), logger.ERROR)
  357. return False
  358. return True