Skip to content
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

Cover art support #278

Merged
merged 86 commits into from
Nov 5, 2014
Merged

Cover art support #278

merged 86 commits into from
Nov 5, 2014

Conversation

cardinot
Copy link
Contributor

More information about the project:
http://mixxx.org/wiki/doku.php/cover_art_support

Skin for tests:

  • Deere1280x800-WXGA

Supported features:

  • analysis
  • autodj
  • crates
  • library
  • history
  • playlist

First Part

  • implement database scheme
  • search for embedded cover
  • search for cover in track directory
  • save covers in disk cache
  • search for covers in disk cache
  • minimal ui (display covers in lower left corner)
  • handle existing users
  • LRU cache
  • unit tests
  • md5 hash
  • do not show cover widget for not supported features:
    • banshee
    • browse
    • itunes
    • recordings
    • rhythmbox
    • traktor

Second Part
cardinot#1

  • redesign dlgtrackinfo to show covers
  • load cover in dlgtrackinfo
  • edit cover location in dlgtrackinfo
  • unset cover (load default)
  • reload cover from metadata
  • warning box issue
  • icon for each cover_art action
  • copy cover from external dir to track dir (if the user changes the cover)

Third Part
cardinot#2

  • add cover_art column in Library feature
  • add cover_art column in AutoDJ feature
  • add cover_art column in Analyze feature
  • add cover_art column in Playlists feature
  • add cover_art column in Crates feature
  • adjust cover art size
  • delay cover loading when the user is scrolling fast
  • batch updates (avoiding doing many writes in a short time)

);
query.bindValue(":id", trackId);

if (query.exec()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bit style nitpicking. Can you please change this to our default style of

if (!query.exec()) {
  LOG_FAILED_QUERY(query);
  return coverArtInfo();
}

// handle query results

@kain88-de
Copy link
Member

from a first rough scanning I have 2 points that you still need to deal with

  • add wcoverart for all features
    • hidden
    • missing
    • playlists
    • crates
    • analysis
    • autodj
    • library
  • add unit-tests

@kain88-de
Copy link
Member

@rryan can we cut a 1.12 branch before we merge this? Currently this is still incomplete because there is no way for a user to change the cover (that would in the next PR) and concentrates on how covers are handled internally. Also this has likely still bugs and definitely is against our feature freeze.

@cardinot
Copy link
Contributor Author

-add wcoverart for all features

  • hidden
  • missing

Done! 2be0d8b

  • playlists
  • crates

It's already working, isn't it?

@@ -27,6 +27,9 @@ DlgAnalysis::DlgAnalysis(QWidget* parent,
connect(m_pAnalysisLibraryTableView, SIGNAL(loadTrackToPlayer(TrackPointer, QString)),
this, SIGNAL(loadTrackToPlayer(TrackPointer, QString)));

connect(m_pAnalysisLibraryTableView, SIGNAL(loadCoverArt(QString, int)),
this, SIGNAL(loadCoverArt(QString, int)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment why this is required?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving to focusedTrackChanged() would clarify this as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent class we use for library models is pretty stupid. This signal is needed so that we can inform wcoverart that a different track is being selected. 'focusedTrackChanged' is more descriptive so we could change it

@daschuer
Copy link
Member

Hi @cardinot

Well done! It looks really nice.

However, I have already commented some code with ideas to make it more streamline.
I will continue my review tonight. And do some tests as well.

Kind regards,

Daniel

return result;
}

CoverArtDAO::coverArtInfo coverInfo = m_pCoverArtDAO->getCoverArtInfo(trackId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not good. You are calling a functions across threads and pass data between them. This could end that you potentially do really a LOT of parallel queries. I don't know how sqlite deals with that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's true... but we could do it outside this thread (into requestPixmap) and send coverInfo as a parameter of loadImage...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then you would to a query everytime you want to load a non cached picture. I thought the goal was to reduce db queries for normal loading

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use QFile::exists(coverLocation) and just fill a CoverArtDAO::coverArtInfo from DB if it not exists...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It means, something like:

CoverArtDAO::coverArtInfo coverInfo;
if (!QFile::exists(coverLocation)) {
     coverInfo = m_pCoverArtDAO->getCoverArtInfo(trackId);
}

As all covers that were loaded correctly once would be stored in our disk-cache, it would return true most of times and we would be doing the db query just when it would be useful for loadImage()...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no Db queries outside of the UI-Thread! Especially with the highly parallel asynchronous Qt:Concurrent system. It is too easy for this to introduce race-condition bugs. I'd rather have that we avoid them when we can.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no Db queries outside of the UI-Thread!

But my suggestion is move it to requestPixmap() to run in the main thread... so, the code that I suggested was to be placed to run in the main thread...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, just keep in mint that there is a pending pull request from last year, that moves all db quires into a dedicated thread.
I hope we can merge it in the 1.13 release, so there is no need to worry that much here to use the DB from the GUI thread.

And yes, db queries can involve HD access. I just wander if it is required to cache all our date in ram, rather than using sqlite cache facilities, but that is an other topic. (I am no sqlit expert)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok sorry I got you wrong. Yeah inside of the UI-Thread it is ok

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, just keep in mint that there is a pending pull request from last year, that moves all db quires into a dedicated thread.

But regardless of that the code will still be easier to understand if we don't do everything in 'loadImage' and we would benefit from it either way.

@daschuer
Copy link
Member

just added the cover arts to developer_skins/Shade_dev to be able to test on my netbook.

@daschuer
Copy link
Member

The header for the cover location in "26". It seams a proper text is missing.

@cardinot
Copy link
Contributor Author

The header for the cover location in "26". It seams a proper text is missing.

sorry, but I didn't get your point...

@kain88-de
Copy link
Member

What I had in mind was more along these lines. For each function I can see from the name what it is supposed to do. The control flow in requestPixmap is easy to understand.

void requestPixmap(id, loc) {
  // check if we are already looking for this tracks cover

  if loc.isEmpty():
    // get track information from coverArtDAO
    QtConcurrent::run(searchForImage, coverInfo)
    // set up future watcher and connect finished signal to 'imageFound'
  else:
    QtConcurrent::run(loadImage, loc, id)
    // set up future watcher and connect finished signal to 'imageLoaded'
}

coverTuple loadImage(loc, id) {
  img = QImage(loc)
  return id, img
}

coverTuple searchForImage(coverInfo) {
  // look in disk-cache
  // look in id3tags
  // look in track directory

  // save image in disk-cache
  return id, img, loc
}

void imageLoaded(id, img) {
  // extract result from sender
  // add to cache
  // remove id from runningIDs
  emit pixmapFound(id, pixmap)
}

void imageFound(id, img, loc) {
  // extract result from sender
  // add image to cover_art table
  // update library table
  // add to cache
  // remove id from runningIDs
  emit pixmapFound(id, pixmap)
}

@cardinot
Copy link
Contributor Author

Hello @kain88-de, thanks for your approach. =D

  if loc.isEmpty():
    // get track information from coverArtDAO
    QtConcurrent::run(searchForImage, coverInfo)
    // set up future watcher and connect finished signal to 'imageFound'
  else:
    QtConcurrent::run(loadImage, loc, id)
    // set up future watcher and connect finished signal to 'imageLoaded'

What about files which are externally removed?
The inicial coverLocation could be notEmpty and it not means that the cover would be loaded. That's the reason why I suggested check if the file exists...

coverTuple searchForImage(coverInfo) {
  // look in disk-cache
  // look in id3tags
  // look in track directory

  // save image in disk-cache
  return id, img, loc
}

Actually we'd be doing it:

coverTuple searchForImage(coverInfo) {
  // look in disk-cache  +  QImage(coverInfo.coverLocation);  [1]
  // look in id3tags  +  return QImage  [2]
  // look in track directory  +  QImage(coverLocation);  [3]

  // save image in disk-cache
  return id, img, loc
}

Note that we will be handling and loading QImages anyway during the search and other VERY important point is that the first step (look in disk cache) is exactly the same thing that you would be doing in the another thread (loadImage(loc, id))... So, IMHO there's no advantage in split it...

I also noticed that the only difference between imageFound and imageLoaded is the db_updating stuff, but it's also very easy to know if we have or not to update it...

Have you seen my last refactoring around this stuff?

@daschuer
Copy link
Member

The header for the cover location in "26". It seams a proper text is missing.

sorry, but I didn't get your point...

The header in the library table for the new cover location column-

@kain88-de
Copy link
Member

The header in the library table for the new cover location column-

Oh that should be hidden from the user. @cardinot you can change this by adding the column to the isColumnInternal function of the tablemodels.

@kain88-de
Copy link
Member

@cardinot I have looked at your recent changes and I know that my approach has a little bit duplicate code. But I have to make just one decision where as in your code you have to make the same decision 3 times. Here is a little ascii-art to show you what I mean.

               Mine                                                    Yours
                   |                                                           |
                  /\                                                          /\
                /    \                                                      /    \ 
                |     |                                                         |
                |     |                                                        /\
                |     |                                                      /    \
                |     |                                                         |
                |     |                                                        /\
                |     |                                                      /    \

Maybe I'm a bit biased but 6 months from now I could understand my control-flow easier and faster then yours. Each function has one well defined task and the decision is happening just once. We've already been clever by using QtConcurrent, now we have to think what would be the easiest maintainable version of this code. @daschuer any comment on this?

Another plus side to this approach is that it is easily testable. If you have a function that behaves differently on different input you have to test all possible input combinations, which is a combinatorial
problem that grows fast.

To check of we need to search for the cover you can also use loc.isEmpty() && !QFile::exists(loc), the extra check for the empty string is not really necessary but the verbosity of the statement is nice. You could also add another function that accepts an id and pixmap which will add the pixmap to the cache and emit the pixmapFound signal to avoid some code duplication.

@cardinot
Copy link
Contributor Author

The header in the library table for the new cover location column-

Oh that should be hidden from the user. @cardinot you can change this by adding the column to the isColumnInternal function of the tablemodels.

Thanks @kain88-de and @daschuer
I pushed it here: 1e2b71e. Please, have a look if it's enough to sort it out! =D

Now only loads a cover once for rows that share the same hash.
Sorts by the hash. It works well except for the case where cover art is
similar but not exactly the same. I think it's good enough to include,
even if some users may be confused why visually similar cover art
doesn't sort together.
Improves scrolling performance when scrolling via keyboard.
@rryan
Copy link
Member

rryan commented Oct 30, 2014

Note, there is discussion about this happening in: cardinot#22

(People subscribing to mixxxdj/mixxx won't see it since it's in @cardinot's fork)

@kain88-de kain88-de merged commit 76113d0 into mixxxdj:master Nov 5, 2014
m0dB pushed a commit to m0dB/mixxx that referenced this pull request Jan 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants