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

expose current track metadata to external APIs #3483

Draft
wants to merge 108 commits into
base: main
Choose a base branch
from

Conversation

daschuer
Copy link
Member

This is a rebase of @davidhm Livemetadata #1675 PR on current main.

It fetaures:

  • MPRIS D-Bus interface for Linux system tray remotes
  • Scrobbling to Listenbrains
  • Writing a NowPlaying.txt

@daschuer
Copy link
Member Author

All green now. @poelzi Please give this a try.

Copy link
Contributor

@poelzi poelzi left a comment

Choose a reason for hiding this comment

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

Smaller nitpicks and the DEBUG ASSERT needs to proper handle fallback.
Have not tested listenbrainz, but mrpis works nice. Also tested the dbus api.

m_fileContents.artist = pTrack->getArtist();
QString writtenString(m_latestSettings.fileFormatString);
writtenString.replace("$author", pTrack->getArtist())
.replace("$title", pTrack->getTitle()) += '\n';
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use '{{ track.title }}' and '{{ track.artist }}' here ?
I'm working on better track export functionality and have added the grantlee library for that, so you can define patterns that will become the path name for the exported files. With this syntax we will later be able to switch to a grantlee template and have the full template power later.

}

void MediaPlayer2::Quit() {
QApplication::quit();
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to call the MixxxApplication to quit here otherwise the controlobjects are not destroyed in order, cause lots of Debug Asserts

}

void MediaPlayer2::Raise() {
m_pMixxx->raise();
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not work when called over dbus.

QStringList ret;
ret << "audio/mpeg"
<< "audio/ogg";
return ret;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we build something from the supported codecs ? At least wav, flac, mp3, aiff should be in the list


namespace {

const QString kPlaybackStatusPlaying = "Playing";
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const QString kPlaybackStatusPlaying = "Playing";
const QLatin1String kPlaybackStatusPlaying = "Playing";

metadata.insert("mpris:length", m_currentMetadata.trackDuration);
metadata.insert("xesam:artist", m_currentMetadata.artists);
metadata.insert("xesam:title", m_currentMetadata.title);
metadata.insert("mpris:artUrl", m_currentMetadata.coverartUrl);
Copy link
Contributor

Choose a reason for hiding this comment

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

https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/#index4h4
I think those would be interesting as well:

album -> xesam:album
rating -> xesam:userRating
play_counter -> xesam:useCount

}

qlonglong MprisPlayer::seek(qlonglong offset, bool& success) {
if (autoDjIdle()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should use canSeek() here

}

bool MprisPlayer::canSeek() const {
return autoDjIdle();
Copy link
Contributor

Choose a reason for hiding this comment

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

this check is wrong when the last track is playing and autodj is already off. you can still seek in the track running.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it is also wrong if auto DJ is off and user plays a track manually.
I think a fix will be part of general overhaul.

if (playingDeck == nullptr) {
ControlProxy playing(ConfigKey(m_pausedDeck, "play"));
BaseTrackPlayer* player = m_pPlayerManager->getPlayer(m_pausedDeck);
DEBUG_ASSERT(player);
Copy link
Contributor

Choose a reason for hiding this comment

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

I had one crash here that I could not reproduce later:

3:41:31.777 [Main] debug TrackDAO::updateTrack  TrackDAO: Updating track in database 7726 QFileInfo(/home/poelzi/Music/psyblast/[Omveda Records]/Omveda Records - XIII Monkeys - Psychedelic Renaissance (FREE)/XIII MONKEYS - XIII Monkeys - Psychedelic Renaissance (FREE) - 01 Alien Agenda.flac)
3:41:31.777 [Main] debug mixxx::Logger::debug  SqlTransaction - Started new SQL database transaction on "MIXXX-1"
3:41:32.131 [Main] debug mixxx::Logger::debug  SqlTransaction - Committed SQL database transaction on "MIXXX-1"
3:41:32.131 [Main] debug BaseTrackCache::updateIndexWithQuery  BaseTrackCache(0xb16c2e0) updateIndexWithQuery took 0 ms
3:41:32.131 [Main] debug mixxx::Logger::debug  GlobalTrackCache - Deleting Track(0x7f7ba83fb480)
3:41:32.134 [Main] debug AutoDJProcessor::calculateTransition  player "[Channel2]" calculateTransition()
3:41:32.134 [Controller] debug unknown  CDBG XONE:K2 MIDI 1: outgoing: status 0x9E (ch 15, opcode 0x9), ctrl 0x2D, val 0x7F
3:41:32.134 [Controller] debug unknown  CDBG XONE:K2 MIDI 1: outgoing: status 0x9D (ch 14, opcode 0x9), ctrl 0x2C, val 0x7F
3:41:32.134 [Controller] debug unknown  CDBG XONE:K2 MIDI 1: outgoing: status 0x9C (ch 13, opcode 0x9), ctrl 0x2C, val 0x7F
3:41:32.134 [Controller] debug unknown  CDBG XONE:K2 MIDI 1: outgoing: status 0x9B (ch 12, opcode 0x9), ctrl 0x2E, val 0x7F
3:41:32.134 [Controller] debug unknown  CDBG XONE:K2 MIDI 1: outgoing: status 0x9A (ch 11, opcode 0x9), ctrl 0x2D, val 0x7F
3:41:32.134 [Controller] debug unknown  CDBG XONE:K2 MIDI 1: outgoing: status 0x99 (ch 10, opcode 0x9), ctrl 0x2C, val 0x7F
3:41:32.141 [Main] debug mixxx::Logger::debug  AnalyzerThread - Enqueueing next track 7728
3:41:32.141 [AnalyzerThread 0 #1] debug mixxx::Logger::debug  AnalyzerThread - Dequeued next track 7728
3:41:32.141 [AnalyzerThread 0 #1] debug mixxx::Logger::debug  AnalyzerThread - Analyzing QFileInfo(/home/poelzi/Music/psyblast/[Omveda Records]/Omveda Records - XIII Monkeys - Psychedelic Renaissance (FREE)/XIII MONKEYS - XIII Monkeys - Psychedelic Renaissance (FREE) - 02 Psychedelic Renaissance.flac)
3:41:32.142 [AnalyzerThread 0 #1] debug mixxx::Logger::debug  SoundSourceProxy - SoundSourceProvider "Xiph.org libFLAC" created a SoundSource for file "file:///home/poelzi/Music/psyblast/[Omveda Records]/Omveda Records - XIII Monkeys - Psychedelic Renaissance (FREE)/XIII MONKEYS - XIII Monkeys - Psychedelic Renaissance (FREE) - 02 Psychedelic Renaissance.flac" of type "flac"
3:41:32.142 [AnalyzerThread 0 #1] debug AnalysisDao::loadAnalysesFromQuery  AnalysisDAO fetched 0 analyses, 0 bytes for track 7728 in 0 ms
3:41:32.143 [AnalyzerThread 0 #1] debug AnalyzerEbur128::initialize  Skipping AnalyzerEbur128
3:41:32.143 [AnalyzerThread 0 #1] debug AnalyzerBeats::initialize  AnalyzerBeats preference settings: 
Plugin: "qm-tempotracker:0" 
Min/Max BPM: 70 250 
Fixed tempo assumption: true 
Offset correction: true 
Re-analyze when settings change: false 
Fast analysis: false
3:41:32.143 [AnalyzerThread 0 #1] debug AnalyzerKey::initialize  AnalyzerKey preference settings: 
Plugin: "qm-keydetector:2" 
Re-analyze when settings change: false 
Fast analysis: false
3:41:32.143 [AnalyzerThread 0 #1] debug AnalyzerKey::shouldAnalyze  Track has previous key detection result that is not up to date with latest settings but user preferences indicate we should not re-analyze it.
3:41:36.193 [AnalyzerThread 0 #1] debug Waveform::toByteArray  Writing waveform from byte array: dataSize 371166 allSignalSize 371166 visualSampleRate 441 audioVisualRatio 100
3:41:36.729 [AnalyzerThread 0 #1] debug AnalysisDao::saveAnalysis  AnalysisDAO saved analysis 7007 "3396322 (1601831 compressed)" bytes for track 7728 in 523 ms
3:41:36.729 [AnalyzerThread 0 #1] debug AnalysisDao::saveTrackAnalyses  Saved waveform analysis for trackId 7728 analysisId 7007
3:41:36.729 [AnalyzerThread 0 #1] debug Waveform::toByteArray  Writing waveform from byte array: dataSize 3840 allSignalSize 3840 visualSampleRate 4.5625 audioVisualRatio 9665.76
3:41:36.741 [AnalyzerThread 0 #1] debug AnalysisDao::saveAnalysis  AnalysisDAO saved analysis 7008 "34941 (15475 compressed)" bytes for track 7728 in 11 ms
3:41:36.741 [AnalyzerThread 0 #1] debug AnalysisDao::saveTrackAnalyses  Saved waveform summary analysis for trackId 7728 analysisId 7008
3:41:36.741 [AnalyzerThread 0 #1] debug mixxx::Logger::debug  AnalyzerWaveform - Waveform generation for track 7728 done 4 s
3:41:37.269 [Main] critical  DEBUG ASSERT: "key.isValid() || flags.testFlag(ControlFlag::AllowInvalidKey)" in function ControlProxy::ControlProxy(const ConfigKey &, QObject *, ControlFlags) at /home/poelzi/Projects/mixxx-test/src/control/controlproxy.cpp:19
3:41:37.270 [Main] critical  DEBUG ASSERT: "player" in function void MprisPlayer::play() at /home/poelzi/Projects/mixxx-test/src/broadcast/mpris/mprisplayer.cpp:203
Segmentation fault (core dumped)

@poelzi
Copy link
Contributor

poelzi commented Dec 28, 2020

@daschuer thanks for picking this one up, it would have taken quite some time for me. apart from smaller suggestions and one crash, this branch works nice.

@daschuer
Copy link
Member Author

Now the crash is fixed.
Mpirs player should now work at any Auto-DJ state, fixing the reported issue.
I have also used [Master],gain for mpris volume, because it creates weird situations when messing with the deck volume.

@Be-ing Be-ing added this to the 2.4.0 milestone Dec 30, 2020
@Be-ing Be-ing changed the title Rebase of Livemetadata #1675 expose current track metadata to external APIs Dec 30, 2020
@Be-ing
Copy link
Contributor

Be-ing commented Dec 30, 2020

Please add comments in the header of every new class explaining its purpose.

// they are connected to is also deleted).
// http://doc.qt.io/qt-5/qdbusabstractadaptor.html
new MediaPlayer2(pMixxx, this);
m_busConnection.registerObject("/org/mpris/MediaPlayer2", this);
Copy link
Contributor

Choose a reason for hiding this comment

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

QLatin1String?

Copy link
Member

@Holzhaus Holzhaus Dec 30, 2020

Choose a reason for hiding this comment

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

IIRC QLatin1String can be used when comparing a QString to a literal value. When constructing an actual QString, I think QStringLiteral should be used. Here we want the latter.

Comment on lines 19 to 26
// the playback will stop when there are no more tracks to play
const QString kLoopStatusNone = QStringLiteral("None");
// The current track will start again from the begining once it has finished playing
const QString kLoopStatusTrack = QStringLiteral("Track");
// The playback loops through a list of tracks
const QString kLoopStatusPlaylist = QStringLiteral("Playlist");
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't these be translated?

Copy link
Contributor

Choose a reason for hiding this comment

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

no, those are status flags for the dbus api

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay. It would help to add a comment to clarify.


ScrobblingManager::ScrobblingManager(UserSettingsPointer pConfig,
std::shared_ptr<PlayerManager> pPlayerManager,
MixxxMainWindow* pWindow)
Copy link
Contributor

Choose a reason for hiding this comment

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

unused, please remove. Let's decouple application logic from QWidgets as much as possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is used for the window control in mpris.

@@ -114,7 +115,7 @@ void CoreServices::initializeSettings() {
m_pSettingsManager = std::make_unique<SettingsManager>(settingsPath);
}

void CoreServices::initialize(QApplication* pApp) {
void CoreServices::initialize(QApplication* pApp, MixxxMainWindow* pMixxx) {
Copy link
Contributor

@Be-ing Be-ing Dec 30, 2020

Choose a reason for hiding this comment

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

Revert this! This defeats the purpose of the CoreServices class.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is used for the window control in mpris.

Copy link
Contributor

Choose a reason for hiding this comment

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

Coupling CoreServices to QWidgets is a big step backwards. Please find another way to implement this. Using signals and slots instead could work.

Copy link
Member Author

Choose a reason for hiding this comment

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

That is not an issue, because it is only the forward declarated class. CoreServices passes only the pointer, without caring what it is.

Copy link
Contributor

Choose a reason for hiding this comment

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

It is an issue to knowingly create problems for other branches in progress.

Copy link
Member Author

Choose a reason for hiding this comment

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

Please explain the issue that it creates.

We only pass a pointer. This can be even a void pointer in CoreService and we can cast it MixxxMainWindow in MPRIS. But making use of the compilers type safety is better.

@@ -22,6 +22,8 @@ class VinylControlManager;
class TrackCollectionManager;
class Library;
class LV2Backend;
class MixxxMainWindow;
Copy link
Contributor

Choose a reason for hiding this comment

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

revert

Copy link
Member Author

Choose a reason for hiding this comment

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

It is used for the window control in mpris.

}

void MediaPlayer2::setFullscreen(bool fullscreen) {
m_pMixxx->slotViewFullScreen(fullscreen);
Copy link
Contributor

Choose a reason for hiding this comment

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

The QWidget dependency can be removed from this class by emitting a signal here

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we can emit a signal here, the issue is that someone needs to connect the MediaPlayer2 signal to the window.
This way we get mpris internals into the window class which is also a bad idea.

In addition we have also:
m_pMixxx->isFullScreen();
m_pMixxx->raise();

As info in the reverse direction.

Form this point of view the solution is not bad.
If we have one day another window technology we "just" need to implement a common interface for both technologies.
For now the current solution is just fine, because we don't know the requirements of the other window system.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can do some proper common interface when the QMLApplication is in some proper state.

Copy link
Contributor

Choose a reason for hiding this comment

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

I disagree. We know this conflicts with #3638. That IMO must be resolved before merging this.

}

bool MediaPlayer2::fullscreen() const {
return m_pMixxx->isFullScreen();
Copy link
Contributor

Choose a reason for hiding this comment

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

How can we get this information without m_pMixxx? Or maybe remove it? Is it really needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

we could track the signals from the mainwindow whne switching from/to fullscreen

Copy link
Contributor

Choose a reason for hiding this comment

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

I like that idea.

Copy link
Member Author

Choose a reason for hiding this comment

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

Due to the connect call we get the coupling anyway. So there is not really a benefit using the complexity of signal and slots.

</rect>
</property>
<property name="windowTitle">
<string>Metadata Broadcast Preferences</string>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<string>Metadata Broadcast Preferences</string>
<string>Metadata Output Preferences</string>

I think the conflation of these new features with broadcasting would confuse a lot of users.

<item>
<widget class="QCheckBox" name="enableFileListener">
<property name="text">
<string>Enable metadata file</string>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<string>Enable metadata file</string>
<string>Write metadata of current track to a file</string>

<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="filePathButton">
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels odd on the left of the QLineEdit. It is conventional to put it on the right.

<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>User token. If you don't have one please register &lt;a href=&quot;https://listenbrainz.org/login/&quot;&gt;here&lt;/a&gt;</string>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<string>User token. If you don't have one please register &lt;a href=&quot;https://listenbrainz.org/login/&quot;&gt;here&lt;/a&gt;</string>
<string>User token. If you do not have one, &lt;a href=&quot;https://listenbrainz.org/login/&quot;&gt;register a ListenBrainz account&lt;/a&gt;.</string>

@github-actions github-actions bot added autodj preferences broadcast Bugs pertaining to streaming radio broadcaster use-case soundio qml labels Mar 22, 2024
@madorian
Copy link

madorian commented Jul 6, 2024

I'm trying to read this, but I'm not really understanding it;) Is this the only way to remotely control Mixxx? I want to load a file into one of its decks using an API.

@daschuer
Copy link
Member Author

daschuer commented Jul 6, 2024

This PR needs some love to split it in review-able chunks to become part of the main branch. I would love to do it one day, but dragged away from it once again.

Do you have some spare time and programming skills?
Than we can implement your request, working together.

First I like to understand your use case? How do you plan to select the track? Do you use a different process that manages your tracks ? On which OS are you?

@madorian
Copy link

madorian commented Jul 6, 2024

I feel you; too many balls in the air here, as well;)

I was just planning to escape from it all mixing some music;). I intend to find tracks using Emacs or CLI, then issue a command of some sort to load it into Mixxx. I'm on NixOS.

@Swiftb0y
Copy link
Member

Swiftb0y commented Jul 6, 2024

@madorian Have you read some of the more advanced techniques for finding tracks within the mixxx library? https://manual.mixxx.org/2.4/en/chapters/library#finding-tracks-search

@daschuer
Copy link
Member Author

daschuer commented Jul 7, 2024

using Emacs or CLI

That could work and is kind of straight forward. So we need a mixxx_cli process that contacts the main mixxx process via a IPC interface.

The project can be split into two parts:

  1. Design the CLI API
  2. Design the IPC

For 1. Go ahead. Pandora's Box is open. What do you need? You may describe it as https://github.com/mixxxdj/proposals

2.: I would prefer to use a standard interface that can be used elsewhere. Like OSC or MPRiS.

Will this work for you?

@madorian
Copy link

madorian commented Jul 7, 2024

@daschuer Yes, OSC is perfect and portable. I'm not familiar with MPRiS, but it seems very cool and I will read up on that.

@acolombier
Copy link
Member

acolombier commented Sep 14, 2024

I just gave that a go and it work great! I haven't had a chance to look into the code much yet, but I have some early feedback on the feature

  • The setting panel could use a better design
    image
    1. The setting category could be perhaps more generic (e.g Integration). I'm thinking of building a custom D-Bus interface in Mixxx which would allow CO to be invoked by other apps, and this would be the kind of category to have both settings in.
      A dedicated icon would be great. Perhaps @ronso0 you have some suggestion here?
    2. Doesn't look like the UI is scaling correctly
    3. It would be great to grey out the label as if the setting is disabled. It might also be worth using group box to highlight best the root checkboxes
    4. Description (if you don't... and The strings...) could look better as a separate QLabel with a smaller font, under the InputBox IMO
  • It would be great to get to support mpris:artUrl. (Looking at the doc, it looks like the covert art could be saved as a temp file)

Also, as an early feedback on the code

Please add comments in the header of every new class explaining its purpose.

This would be quite useful as this is adding a significant amount of file in the repo.

Appreciate you may not be interested in finishing this PR, let me know if so and I'd be interested to help!

Edit: Just noticed that mpris:arturl is supposed to already be implemented, but doesn't seem to work. I also think you will need individual URL for remote app to reload the new cover.

@daschuer
Copy link
Member Author

I am very much interested to get this PR merged. My plan was to move the features into single PRs to make them review-able. But I got dragged away. Any help is welcome. My current priority is to release 2.4.2 and 2.5.0.

@acolombier
Copy link
Member

Sounds good - glad to hear this is still alive! I'm removing needs review for now and making it a draft till this gets broken down into multiple PR.

@acolombier acolombier marked this pull request as draft October 27, 2024 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.