New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PICARD-1045: Add program update checking #904
Conversation
I would like Picard Website to have an end point to control updates instead. I think you made a PR for it a while back. Why not reopen that? @zas any thoughts on Gh vs PW for update checking? |
Using a PW endpoint would make things much more simpler, using GH leads to a lot of extra code. |
picard/util/checkupdate.py
Outdated
self._update_level = update_level | ||
|
||
# Gets list of releases from GitHub website api. | ||
output_text = _("Getting release information from GitHub.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and below is exactly the code from query_available_updates
, should call that instead of reimplementing it here,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. That was a left over from a previous version and was supposed to have been removed. I'll make the change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change made, but I ended up having to force push it. You should be able to see the corrected version in the current changes.
I don't think that's much the case. Given the pull request the actual Github specific code to handle the github request and the response is something below 20 LOC in total. The rest is UI and general update handling + logging. Our own implementation might be simpler, especially as we could give back an optimized result from PW (e.g. return only the latest version), so the code to handle the response would be simplified a bit. But on the other side there is additional code on PW. I actually would go with the GH pages. Of course it would also be possible to revive https://github.com/metabrainz/picard-website/pull/112/files and adapt this PR to use that endpoint, I don't think that would require much code changes. |
It's funny because the reason that I abandoned that PR was because of zas's comment about using GitHub instead. I assumed that using the GitHub api would result in less ongoing maintenance work because the release information would only need to be maintained in one place (i.e.: GitHub because that's where the releases were being managed). If the consensus is to use PW instead, I'll resurrect the other PR and make the necessary changes here. |
Picard may not be hosted on Github in the future (Microsoft bought it...) |
@phw, The changes have been made to remove the duplicated code as requested, but I ended up having to force push it. You should be able to see the corrected version in the current changes. |
@zas / @samj1912, I've changed the update version information source from GitHub to picard-website as requested. I've tested it locally based on the current code in picard-website PR #119, but won't be able to do any production testing until the picard-website PH has been merged. I thought I would post the updated code for this PR now so that we can get a head start on any reviews and requested changes, at your convenience. |
Code is looking good to me. I would like to run the code though to see it in action. Will do so later this week |
9c39575
to
bb947be
Compare
picard/ui/options/general.py
Outdated
@@ -45,6 +45,10 @@ class GeneralOptionsPage(OptionsPage): | |||
config.TextOption("persist", "oauth_access_token", ""), | |||
config.IntOption("persist", "oauth_access_token_expires", 0), | |||
config.TextOption("persist", "oauth_username", ""), | |||
config.BoolOption("setting", "check_for_updates", False), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe set this to true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree - true would be a better default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll make the change.
picard/ui/options/general.py
Outdated
@@ -45,6 +45,10 @@ class GeneralOptionsPage(OptionsPage): | |||
config.TextOption("persist", "oauth_access_token", ""), | |||
config.IntOption("persist", "oauth_access_token_expires", 0), | |||
config.TextOption("persist", "oauth_username", ""), | |||
config.BoolOption("setting", "check_for_updates", False), | |||
config.IntOption("setting", "update_check_days", 0), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this to 10?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest 7 or 14 rather than 10.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll set the initial value to 7.
picard/util/checkupdate.py
Outdated
) | ||
if self._show_always: | ||
msg_title = _("Picard Update") | ||
msg_text = _("Unable to retrieve the latest version informarmation.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
informarmation
-> information
;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. Fat fingers... 😁
important=True | ||
) | ||
|
||
def _releases_json_loaded(self, response, reply, error, callback=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callback
is unused here.
I think it should be called, if set, and error/success passed to it, so you can move config.persist['last_update_check'] = datetime.date.today().toordinal()
out of this (dropping dependency on datetime module), and move it to the caller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I've made the change as requested. If I've misunderstood, please let me know. Thanks.
picard/ui/mainwindow.py
Outdated
@@ -147,6 +147,9 @@ def keyPressEvent(self, event): | |||
def show(self): | |||
self.restoreWindowState() | |||
super().show() | |||
if config.setting['check_for_updates'] and datetime.date.today().toordinal() >= config.persist['last_update_check'] + config.setting['update_check_days']: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
datetime.date.today().toordinal() >= config.persist['last_update_check'] + config.setting['update_check_days']
If update_check_days
is 0, and >=
is used then it will check more than once per day.
So i think it should be >
+ ensure update_check_days
is >= 0
Or >=
and ensure update_check_days
is >= 1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the zero interval I had intended to allow multiple checks per day, but I see now that might not be appropriate or required in a production environment. I think your second suggested option is better because it allows checks on successive days with the 'update_check_days' setting at 1, whereas the first option will only do checks on successive days if the setting is 0. It all comes down to how you interpret the meaning of 'update_check_days'. The interpretation when I was writing it was that a value of 1 meant to check every 1 day (i.e.: every day). Following this logic, I also suggest that the lower limit in the settings be changed from 0 to 1 for the 'update_check_days' setting.
@zas / @samj1912 / @phw / @Sophist-UK I think I have now captured all of your requested changes based on my understanding of the discussion. Please have a look at your convenience and let me know if I've misunderstood or missed something. Thanks. |
985b648
to
2843f7e
Compare
If / when this is accepted, I have also made changes to the documentation on the picard-website and will submit a PR at that time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, this is looking really good now. Just a minor thing about specifying the parent window for the dialog boxes. Apart from that I am all for merging this.
picard/util/checkupdate.py
Outdated
if key: | ||
msg_text = _("A new version of Picard is available.\n\nOld version: %s\nNew version: %s\n\n" | ||
"Would you like to download the new version?") % (PICARD_FANCY_VERSION_STR, self._available_versions[key]['tag'],) | ||
if QMessageBox.information(None, msg_title, msg_text, QMessageBox.Ok | QMessageBox.Cancel, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the message boxes should specify the main window as parent. Otherwise the dialog might open completely independent of the main window, as it happened to me on my multi-monitor setup.
You can get the main window with self.tagger.window
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, I think. Since I wasn't importing tagger into the checkupdate module, I included the parent information in the call to the check_update() method call from the main window. Is this an acceptable alternative?
@rdswift Two answers to this :)
|
I never noticed that patch in tagger.py before. I still think I'd rather explicitly pass the parent than rely on a patch. Good point about passing it to the constructor. I'll make the change. |
picard/util/checkupdate.py
Outdated
compare_version_tuples, | ||
load_json, | ||
) | ||
import picard.util.webbrowser2 as wb2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's only one occurence of wb2
and everywhere else we use webbrowser2.open()
, so just do:
from picard.util import (
compare_version_tuples,
load_json,
webbrowser2,
)
....
webbrowser2.open(self._available_versions[key]['urls']['download'])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. The way I had it doesn't make any sense at all. The hazards of copy 'n' paste from another module.
picard/util/checkupdate.py
Outdated
# Valid keys include: 'download' and 'changelog'. The only required key in | ||
# the 'urls' dictionary is 'download'. | ||
|
||
# Initialize empty dictionary for 'stable' key |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this initialization really required ?
I mean you could just initialize to None
and do proper tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not required at all. Will set it to None
and change the test as suggested.
picard/util/checkupdate.py
Outdated
@property | ||
def available_versions(self): | ||
'''Provide a list of the latest version tuples for each update type.''' | ||
return self._available_versions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this property used somewhere ?
Why not rename self._available_versions
to self.available_versions
instead ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it's not used anywhere, so I'll just remove it.
picard/util/checkupdate.py
Outdated
self._parent, | ||
_("Picard Update"), | ||
_("A new version of Picard is available.\n\n" | ||
"Old version: {picard_old_version}\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/Old version/Current version/?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about 'Your version' or 'This version' instead? When I hear the term 'Current version' I automatically think of the latest version. As well, perhaps change "New version" to "Latest version"?
picard/util/checkupdate.py
Outdated
_("A new version of Picard is available.\n\n" | ||
"Old version: {picard_old_version}\n" | ||
"New version: {picard_new_version}\n\n" | ||
"Would you like to download the new version?").format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit bothered by this, because if Picard was installed through a package manager it is a bad idea to suggest the user to download it.
I wonder if we should provide an easy way for packagers to disable this feature...
What do you think guys?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could change this to something like, "Visit the website for more information?", and just link to `https://picard.musicbrainz.org/" or "https://picard.musicbrainz.org/changelog/" for stable releases (set in the PW configuration).
Another option is to leave out the link option entirely, and just have this as a notification only. If the user wants to update, they'll find a way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Linking to the Changelog with some View recent changes
title could do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually would leave it as is for the case that we show update information. As a user I expect the software that has built-in update checks to actually direct me to the download.
But @zas' concern about the need for packagers to disable this is valid. As a packager I ideally would want to have an option when running the install via setup.py to completely disable this feature (no visible option, no update checking performed).
But crippling the displayed message doesn't help both cases: Distribution users still get bothered with update information even though they depend on their distribution to update the packages and have no easy way to update themselves, and users who installed manually get an update information where they then have to hunt for the download page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having said this IMHO we should probably merge this PR and open a separate PR for disabling the update check. I am also not really sure how to technically best solve this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not very familiar with what happens when installing from a packager, but if there is (or could be) a specific file (e.g.: PACKAGER.txt) added to the program directory, we should be able to check for that and disable the update checking and hide the options in the toolbar and settings. Would that work?
I've already coded the changes based on Picard finding a PACKAGER.txt
file, and can include them here if you like. Then the other PR could just sort out the INSTALLED_BY_PACKAGER
test if we wanted something different.
picard/ui/mainwindow.py
Outdated
self.check_for_update(True) | ||
|
||
def auto_update_check(self): | ||
do_auto_update_check = config.setting['check_for_updates'] and config.setting['update_check_days'] > 0 and datetime.date.today().toordinal() >= config.persist['last_update_check'] + config.setting['update_check_days'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use intermediate variables to shorten lines and cache config.
values, this part is hard to read.
check_for_updates = config.setting['check_for_updates']
update_check_days = config.setting['update_check_days']
last_update_check = config.persist['last_update_check']
update_level = config.setting['update_level']
today = datetime.date.today().toordinal()
do_auto_update_check = check_for_updates and update_check_days > 0 and today >= last_update_check + update_check_days
log.debug('{check_status} start-up check for program updates. Today: {today_date}, Last check: {last_check} (Check interval: {check_interval} days), Update level: {update_level} ({update_level_name})'.format(
check_status='Initiating' if do_auto_update_check else 'Skipping',
today_date=datetime.date.today(),
last_check=str(datetime.date.fromordinal(last_update_check)) if last_update_check > 0 else 'never',
check_interval=update_check_days,
update_level=update_level,
update_level_name=PROGRAM_UPDATE_LEVELS[update_level]['name'] if update_level in PROGRAM_UPDATE_LEVELS else 'unknown',
))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the intermediate variables is a lot better. I've made the change and will upload shortly.
The branch looks good to me, just 1 question/doubt of sorts. Do we need to add versioning to the releases API? |
Actually it's a good idea, more future-proof, and consistent with existing stuff. But i agree, |
Why start with v2? |
Ah, I think I see, you consider current |
It is just because we re-use API made for plugins, and last iteration is v2 ... but the main issue here is this re-use. Perhaps we need to think about this stuff, especially because plugins stuff is likely to change (again) soon... |
Are we talking about the releases endpoint on the PW api? At one point I had it set to both |
- Change default values. Use callback for updating last check date. - Add parent window information to dialog box calls - Pass the parent information to the UpdateCheckManager constructor rather than at each call to the check_update() method. - Correct date test to allow automatic update checking on consecutive days when an intervall of 1 day is selected. - Add a new (constant) dictionary of standard update levels - Reformat debug log messages - Use new dictionary to populate update check level combobox
- Move PROGRAM_UPDATE_LEVELS to picard.constants - Logging changes - Formatting and translation clean-up - Change picard.const.PROGRAM_UPDATE_LEVELS to numeric keys - Add releases endpoint to picard.const.PLUGINS_API - Refactor to remove duplicated code in mainwindow.py - General cleanup of debug log translations - Use intermediate variables to cache config information
@samj1912 @phw : Re-add |
@zas I vote for yes. At first I had though the plugin API would be versionless and we would only introduce this version for the releases API, but now I see we have both v1 and v2 for the plugin API. It just makes sense to have this in from the start, because it is not unlikely we will have changes to the structure. |
Do we need v1/releases? |
In my opinion, it should start with If you all agree, I can prepare the picard-website PR and update this PR to match if you like. |
I don't think so. Plus, apart Picard, nothing will use those endpoints for a while. |
Summary
Problem
There is currently no system included to allow checking for available updates, and for automatically notifying the user when a new version of the software is available.
Solution
Added update checking class, which calls the GitHub api to get the latest available versions (i.e.: stable and beta) for Picard and compares them to the version of the currently running instance. If an update is available, the user will be shown a dialog box with an option to download the new version.
Added a manual call to the update checker under the Help section of the main toolbar.
Added two new checkboxes to the General options tab to enable automatic checking during program startup, and whether to also check for beta (dev) versions. Automatic checks will be silent and not display check results if no update is available.
Added one new spinbox to the General options tab to select the number of days to wait between automatic update checks during program startup.
Action
No additional action required.