New plugin "Statistics"

Hi.

I have completed the plugin.

You can select everything in the right panel or any number and order.

After each selection (blue background of the bar) you need to call the plugin.


|

|

Code:

PLUGIN_NAME = "Statistics"
PLUGIN_AUTHOR = "Echelon"
PLUGIN_DESCRIPTION = "Counts the types of albums from the selection in the right Picard panel"
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ['2.2']
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from PyQt5 import QtGui
from PyQt5.QtWidgets import QLabel, QGridLayout, QWidget
from PyQt5.QtGui import QPixmap, QIcon

from picard.ui.itemviews import BaseAction, register_album_action

statwindow = QWidget()
statwindow.setStyleSheet("font-size:12pt;")

grid = QGridLayout()

statwindow.setLayout(grid)
statwindow.setGeometry(750, 200, 400, 150)
statwindow.setWindowTitle("Statistics")
statwindow.setWindowIcon(QtGui.QIcon(":/images/16x16/org.musicbrainz.Picard.png"))

class AlbumCounter(BaseAction):
    NAME = "Statistics"

    def callback(self, objs):
        A = 0
        B = 0
        C = 0
        D = 0
        E = 0
        T = 0

        while grid.count():
            item = grid.takeAt(0)
            widget = item.widget()
            if widget is not None: 
                widget.clear()

        text1 = QLabel("Album unchanged")
        text2 = QLabel("Album modified")
        text3 = QLabel("Album unchanged and complete    ")
        text4 = QLabel("Album modified and complete")
        text5 = QLabel("Album errors")
        text6 = QLabel("Total")

        grid.addWidget(text1, 0, 0)
        grid.addWidget(text2, 1, 0)
        grid.addWidget(text3, 2, 0)
        grid.addWidget(text4, 3, 0)
        grid.addWidget(text5, 4, 0)
        grid.addWidget(text6, 5, 0)

        icon1 = QLabel()
        icon1.setPixmap(QPixmap(":/images/22x22/media-optical.png"))
        icon2 = QLabel()
        icon2.setPixmap(QPixmap(":/images/22x22/media-optical-modified.png"))
        icon3 = QLabel()
        icon3.setPixmap(QPixmap(":/images/22x22/media-optical-saved.png"))
        icon4 = QLabel()
        icon4.setPixmap(QPixmap(":/images/22x22/media-optical-saved-modified.png"))
        icon5 = QLabel()
        icon5.setPixmap(QPixmap(":/images/22x22/media-optical-error.png"))

        grid.addWidget(icon1, 0, 2)
        grid.addWidget(icon2, 1, 2)
        grid.addWidget(icon3, 2, 2)
        grid.addWidget(icon4, 3, 2)
        grid.addWidget(icon5, 4, 2)

        for album in objs:
            if album.errors:
                E = E + 1
            elif album.is_complete():
                if album.is_modified():
                    D = D + 1
                else:
                    C = C + 1
            else:
                if album.is_modified():
                    B = B + 1
                else:
                    A = A + 1

        T = A + B + C + D + E

        text1a = QLabel(str(A))
        text2b = QLabel(str(B))
        text3c = QLabel(str(C))
        text4d = QLabel(str(D))
        text5e = QLabel(str(E))
        text6t = QLabel(str(T))

        grid.addWidget(text1a, 0, 1)
        grid.addWidget(text2b, 1, 1)
        grid.addWidget(text3c, 2, 1)
        grid.addWidget(text4d, 3, 1)
        grid.addWidget(text5e, 4, 1)
        grid.addWidget(text6t, 5, 1)

        statwindow.show()

register_album_action(AlbumCounter())
4 Likes
  1. How submit it as a PR?

Well done on making this work.

  1. You need to log into Github and clone the Picard-Plugins repo, and in your cloned copy…
  2. Add a branch called album_statistics, then add a file called /plugins/album_statistics/album_statistics.py, then copy and paste your code into this and save / commit the file.
  3. Github should then offer you the option of creating a PR and you click the button to do so.

When you submit this as a PR, you will get some review comments, and to help you along, here are mine. Please don’t consider the quantity of these review comments to be a negative response - this seems like a useful plugin and you have successfully written it and made it work.

  1. Currently called Statistics - IMO Albums Statistics is a better name.
  2. “Counts the types of albums” would be better described as “Counts the quality or status of albums”. I also wonder whether an example might be useful in the description e.g. “Unchanged: 5, Modified: 2, Incomplete: 2, Errored: 1, Total: 10.”
  3. IMO icons should either be to the left of the descriptions or between the descriptions and the counts.
  4. I think there should be a description line before the stats that says something like “The status of the selected Albums is as follows:”.
  5. In the descriptions Album should IMO be either Albums or Album(s), or even omitted as it is implied by the Window Title “Albums Statistics”.
  6. I would personally prefer to see the descriptions as e.g. “Albums incomplete & unchanged / saved”, “Albums complete and changed / unsaved” etc.
  7. You have created the statwindow as a global variable and IMO it would be better being inside the main class as a class variable self.statwindow and initialised created in the class constructor.
  8. Class name should IMO be AlbumStats.
  9. Variable names needs a bit of work e.g. statwindow → stats_popup, A/B/C/D/E/TOT/text* need to have names that reflect their purpose. T does not need to be initialised because it is a calculated value, the others can be initialised in a single statement A = B = C = D = E = 0. The text* variables can probably be eliminated e.g. grid.addWidget(QLabel(str(A)), 0, 1)
  10. There is a LOT of repeated code for adding the widget - IMO better to create a small class method that does the several actions for the 3 widgets in each row (e.g. create_row_widgets, and then call this function once for each row with the relevant values passed as parameters.
  11. I do wonder whether there is a better style for the nested if statements.
  12. You are clearing widgets at the start and recreating them. It might be better to create them in the class constructor, saving a pointer to each of the text widgets for later use and simply change the values of the text widgets before you show the window.
  13. Are there any issues with this when it is displayed on either old low-resolution monitors or modern, expensive ultra-high resolution monitors?
  14. I have no idea how I18n (language translation) is handled for plugins, but all string constants should IMO be wrapped in a translation call e.g. _("string"). @outsidecontext Philipp: Can you give any indicator here on how people can add translations to a plugin? Is it possible in Picard v2? Will it be better / easier in Picard v3

Sorry to give so many comments; they may perhaps look daunting but are quite minor and really quite easy to change.

1 Like

@Sophist don’t worry. :wink:

It’s nice that you wrote it without a teacher’s lecture.

We were all novice programmers at one point. (Admittedly, my first programming experience was just over 50 years ago, but even so…)

2 Likes

I don’t know. Maybe set:
statwindow.setGeometry(100, 100, 400, 200)
instead
statwindow.setGeometry(750, 200, 400, 200)

I don’t know either - but when I see “px” or integers without a clear UoM (and I can’t be arsed to look up what the UoM is for that QT method and so assume it is “px” also) I then immediately wonder how it scales.

@Sophist

I haven’t thanked you yet, so thanks. :wink:

I’ve made your corrections.

However, the grid variable is global.

I can’t pass this variable between classes.

The bigger problem is with the clear() command.

t1 = QLabel(str(A))
t1.clear()
grid.addWidget(t1, 1, 1)
t1 = QLabel(str(A))

It doesn’t work. I tried different combinations.

Finally, the entire corrected code.

Please write updated comments if you can.

PLUGIN_NAME = "Albums Statistics"
PLUGIN_AUTHOR = "Echelon"
PLUGIN_DESCRIPTION = "Counts the quality or status of albums"
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ['2.2']
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from PyQt5 import QtGui
from PyQt5.QtWidgets import QLabel, QGridLayout, QWidget
from PyQt5.QtGui import QPixmap, QIcon

from picard.ui.itemviews import BaseAction, register_album_action

grid = QGridLayout()

class Window(QWidget):
    
    def __init__(self):
        super().__init__()

        self.interface()
    
    def interface(self):
        
        self.setStyleSheet("font-size:12pt;")

        self.setLayout(grid)
        self.setGeometry(750, 200, 400, 200)
        self.setWindowTitle("Albums Statistics")
        self.setWindowIcon(QtGui.QIcon(":/images/16x16/org.musicbrainz.Picard.png"))
        
        grid.addWidget(QLabel("The status of the selected Albums is as follows:"), 0, 0, 1, 3)
        grid.addWidget(QLabel("Unchanged"), 1, 2)
        grid.addWidget(QLabel("Modified"), 2, 2)
        grid.addWidget(QLabel("Unchanged and complete"), 3, 2)
        grid.addWidget(QLabel("Modified and complete"), 4, 2)
        grid.addWidget(QLabel("Errored"), 5, 2)
        grid.addWidget(QLabel("Total"), 6, 2)

        icon1 = QLabel()
        icon1.setPixmap(QPixmap(":/images/22x22/media-optical.png"))
        icon2 = QLabel()
        icon2.setPixmap(QPixmap(":/images/22x22/media-optical-modified.png"))
        icon3 = QLabel()
        icon3.setPixmap(QPixmap(":/images/22x22/media-optical-saved.png"))
        icon4 = QLabel()
        icon4.setPixmap(QPixmap(":/images/22x22/media-optical-saved-modified.png"))
        icon5 = QLabel()
        icon5.setPixmap(QPixmap(":/images/22x22/media-optical-error.png"))

        grid.addWidget(icon1, 1, 0)
        grid.addWidget(icon2, 2, 0)
        grid.addWidget(icon3, 3, 0)
        grid.addWidget(icon4, 4, 0)
        grid.addWidget(icon5, 5, 0)
        
        self.show()

class AlbumStats(BaseAction):
    NAME = "Statistics"
    
    def callback(self, objs):
        A = B = C = D = E = 0
        '''
        while grid.count():
            item = grid.takeAt(0)
            widget = item.widget()
            if widget is not None: 
                widget.clear()
        '''        
        for album in objs:
            if album.errors:
                E = E + 1
            elif album.is_complete():
                if album.is_modified():
                    D = D + 1
                else:
                    C = C + 1
            else:
                if album.is_modified():
                    B = B + 1
                else:
                    A = A + 1

        T = A + B + C + D + E
        
                
        t1 = QLabel(str(A))
        t1.clear()
        grid.addWidget(t1, 1, 1)
        t1 = QLabel(str(A))
        
        grid.addWidget(t1, 1, 1)
        grid.addWidget(QLabel(str(B)), 2, 1)
        grid.addWidget(QLabel(str(C)), 3, 1)
        grid.addWidget(QLabel(str(D)), 4, 1)
        grid.addWidget(QLabel(str(E)), 5, 1)
        grid.addWidget(QLabel(str(T)), 6, 1)

window = Window()

register_album_action(AlbumStats())

This “if” condition is taken from the itemviews.py file

if album.errors:
            self.setIcon(MainPanel.TITLE_COLUMN, AlbumItem.icon_error)
            self.setToolTip(MainPanel.TITLE_COLUMN, _("Processing error(s): See the Errors tab in the Album Info dialog"))
        elif album.is_complete():
            if album.is_modified():
                self.setIcon(MainPanel.TITLE_COLUMN, AlbumItem.icon_cd_saved_modified)
                self.setToolTip(MainPanel.TITLE_COLUMN, _("Album modified and complete"))
            else:
                self.setIcon(MainPanel.TITLE_COLUMN, AlbumItem.icon_cd_saved)
                self.setToolTip(MainPanel.TITLE_COLUMN, _("Album unchanged and complete"))
        else:
            if album.is_modified():
                self.setIcon(MainPanel.TITLE_COLUMN, AlbumItem.icon_cd_modified)
                self.setToolTip(MainPanel.TITLE_COLUMN, _("Album modified"))
            else:
                self.setIcon(MainPanel.TITLE_COLUMN, AlbumItem.icon_cd)
                self.setToolTip(MainPanel.TITLE_COLUMN, _("Album unchanged"))

Most people delete the entire widget.
Explanation on the Internet:

" Removing a row or column (or even a single cell) from a QGridLayout is tricky. Use the code provided below."

https://stackoverflow.com/questions/5395266/removing-widgets-from-qgridlayout/19256990#19256990

I can’t do it any better.

Can I submit this?

What are my chances?

PLUGIN_NAME = "Albums Statistics"
PLUGIN_AUTHOR = "Echelon"
PLUGIN_DESCRIPTION = "Counts the quality or status of albums"
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ['2.2']
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from PyQt5 import QtGui
from PyQt5.QtWidgets import QLabel, QGridLayout, QWidget
from PyQt5.QtGui import QPixmap, QIcon

from picard.ui.itemviews import BaseAction, register_album_action

grid = QGridLayout()

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.interface()

    def interface(self):
        self.setLayout(grid)
        self.setGeometry(100, 100, 400, 200)
        self.setWindowTitle("Albums Statistics")
        self.setWindowIcon(QtGui.QIcon(":/images/16x16/org.musicbrainz.Picard.png"))
        self.setStyleSheet("font-size:12pt;")
        self.show()


class AlbumStats(BaseAction):
    NAME = "Statistics"

    def callback(self, objs):
        A = B = C = D = E = 0

# A - An integer variable counting albums Incomplete & unchanged
# B - An integer variable counting albums Incomplete & modified
# C - An integer variable counting albums Complete & unchanged
# D - An integer variable counting albums Complete & modified
# E - An integer variable counting albums Errored
# T - An integer variable summing up all albums

        while grid.count():
            item = grid.takeAt(0)
            widget = item.widget()
            if widget is not None: 
                widget.clear()

        grid.addWidget(QLabel("The status of the selected Albums is as follows:"), 0, 0, 1, 3)
        grid.addWidget(QLabel("Incomplete & unchanged"), 1, 2)
        grid.addWidget(QLabel("Incomplete & modified"), 2, 2)
        grid.addWidget(QLabel("Complete & unchanged"), 3, 2)
        grid.addWidget(QLabel("Complete & modified"), 4, 2)
        grid.addWidget(QLabel("Errored"), 5, 2)
        grid.addWidget(QLabel("Total"), 6, 2)

        icon1 = QLabel()
        icon1.setPixmap(QPixmap(":/images/22x22/media-optical.png"))
        icon2 = QLabel()
        icon2.setPixmap(QPixmap(":/images/22x22/media-optical-modified.png"))
        icon3 = QLabel()
        icon3.setPixmap(QPixmap(":/images/22x22/media-optical-saved.png"))
        icon4 = QLabel()
        icon4.setPixmap(QPixmap(":/images/22x22/media-optical-saved-modified.png"))
        icon5 = QLabel()
        icon5.setPixmap(QPixmap(":/images/22x22/media-optical-error.png"))

        grid.addWidget(icon1, 1, 0)
        grid.addWidget(icon2, 2, 0)
        grid.addWidget(icon3, 3, 0)
        grid.addWidget(icon4, 4, 0)
        grid.addWidget(icon5, 5, 0)

        for album in objs:
            if album.errors:
                E = E + 1
            elif album.is_complete():
                if album.is_modified():
                    D = D + 1
                else:
                    C = C + 1
            else:
                if album.is_modified():
                    B = B + 1
                else:
                    A = A + 1

        T = A + B + C + D + E

        grid.addWidget(QLabel(str(A)), 1, 1)
        grid.addWidget(QLabel(str(B)), 2, 1)
        grid.addWidget(QLabel(str(C)), 3, 1)
        grid.addWidget(QLabel(str(D)), 4, 1)
        grid.addWidget(QLabel(str(E)), 5, 1)
        grid.addWidget(QLabel(str(T)), 6, 1)

w = Window()

register_album_action(AlbumStats())

@outsidecontext

You can also build this plugin into Picard. I will waive all rights. :wink:

Or create the widget once and just change the text bits that change before you pop it.

Pull request: Create albums_statistics.py by Echelon666 · Pull Request #384 · metabrainz/picard-plugins · GitHub

st

This looks like you have create a new widget on top of the old one.

To change the number whilst reusing the widget, when you create the widget you need to save the widget object in a class variable, and then reuse this original widget by using a different call to change the text contents of the widget rather than creating a new one.

@Sophist

Too many variables, classes, objects, calls for me.

It’s all getting mixed up.

I’d like to help myself and you, but I honestly say “I can’t do it”.

It’s a waste of your time and mine.

I haven’t tested this, but based on your code here is how I would have written this:

PLUGIN_NAME = "Albums Statistics"
PLUGIN_AUTHOR = "Echelon"
PLUGIN_DESCRIPTION = "Summarises the status of selected albums e.g. Changed?, Complete? Error?"
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ['2.2']
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from PyQt5 import QtGui
from PyQt5.QtWidgets import QLabel, QGridLayout, QWidget
from PyQt5.QtGui import QPixmap, QIcon

from picard.ui.itemviews import BaseAction, register_album_action

class AlbumsStats(BaseAction):
    NAME = "Albums Statistics"

    def __init__(self):
        # Create grid hidden
        self.grid = QGridLayout()
        self.grid.addWidget(QLabel("The status of the selected Albums is as follows:"), 0, 0, 1, 3)

        self.addGridRow(1, ":/images/22x22/media-optical.png",
            _("Incomplete & unchanged"))
        self.addGridRow(2, ":/images/22x22/media-optical-modified.png",
            _("Incomplete & modified"))
        self.addGridRow(3, ":/images/22x22/media-optical-saved.png",
            _("Complete & unchanged"))
        self.addGridRow(4, ":/images/22x22/media-optical-saved-modified.png",
            _("Complete & modified"))
        self.addGridRow(5, ":/images/22x22/media-optical-error.png",
            _("Errored"))
        self.addGridRow(6, "",
            _("Total"))

        self.grid.addWidget(QLabel("Total"), 6, 2)

        self.window = QWidget()
        self.window.setLayout(self.grid)
        self.window.setGeometry(100, 100, 400, 200)
        self.window.setWindowTitle(_("Albums Statistics"))
        self.window.setWindowIcon(QIcon(":/images/16x16/org.musicbrainz.Picard.png"))
        self.window.setStyleSheet("font-size:12pt;")

    def addGridRow(self, row, icon_location, description):
        icon = QLabel()
        if icon_location:
            icon.setPixmap(QPixmap(icon_location))
        self.grid.addWidget(icon, row, 0)

        self.grid.addWidget(QLabel(""), row, 1)

        self.grid.addWidget(QLabel(description), row, 2)

    def setCounter(self, row, count):
        counter = self.grid.itemAtPosition(row, 1)
        counter.setText(str(count))

    def callback(self, objs):
        incomplete_unchanged = incomplete_modified = complete_unchanged = complete_modified = errored = 0

        for album in objs:
            if album.errors:
                errored += 1
            elif album.is_complete():
                if album.is_modified():
                    complete_modified += 1
                else:
                    complete_unchanged += 1
            else:
                if album.is_modified():
                    incomplete_modified += 1
                else:
                    incomplete_unchanged += 1

        total = incomplete_unchanged + incomplete_modified + complete_unchanged + complete_modified + errored

        self.setCounter(1, incomplete_unchanged)
        self.setCounter(2, incomplete_modified)
        self.setCounter(3, complete_unchanged)
        self.setCounter(4, complete_modified)
        self.setCounter(5, errored)
        self.setCounter(6, total)

        self.window.show()

register_album_action(AlbumsStats())
1 Like

@Sophist

Why did you write on Github:

“I have no idea why the creator of this PR has chosen to submit the PR without making any changes I suggested, however…”

ANY ???

I’ve already implemented 12 of your corrections the first time.

Only without “selfstatwindow” and clearing the results.

In essence you said that you weren’t able to implement my review comments because of your inexperience.

I wanted to help you and so I spent more of my own time showing you what my review comments were about in the hope that the code would be more readable and maintainable and so more likely to get approved and made generally available.

I made some generally positive comments about your work, and made a total of 12 review comments, and the bulk of these were not implemented, so it is difficult to see how you can realistically claim that you implemented all 12 of them - and indeed you stated yourself that you were unable to implement them. Your original code, my review comments, your revised code and my revised version are all available above for people to review for themselves and form their own judgements.

I was only trying to help get your vision approved - and now I just wish I hadn’t bothered to try to help.

2 Likes