-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[WIP] Livemetadata PR #1675
[WIP] Livemetadata PR #1675
Changes from 14 commits
32cece7
ba829a2
5169748
3d8c501
523d09b
23ef862
0a4a4d6
d203e85
1a6bc0d
ff27795
db2db23
7f55150
121443e
2cd5bbc
b9c4a6c
e10df9e
c1de7db
50019b0
d805306
4e714d5
b3f8ea8
8cd9c16
d4b9aa5
ec98ef6
0209b4a
2d8f4a2
88c4819
ab8b524
97daf77
4bc2de3
49b8b93
567e9b5
5748fa9
3a677b6
9c43d09
2c21183
a6d67d7
ba8a6fc
6b7f207
f7c6a83
1057b06
152925c
ad43628
751f567
64d35c5
ddfb859
e4dc8df
a1ad4c6
ee4c5a5
4571d43
185d2f2
0bc22be
0af2279
6be0042
62eae49
c2317c7
8b5e54a
cac8c1f
6807838
d6880a1
32e565a
1b77c8b
7ba10a7
94ab525
5e8fb60
41bdfb9
0def3e3
9fe5277
97271d8
f4b9649
dde6c47
e1cfd7a
69a3020
7af4dd7
170e6c9
8c6fa58
899f207
4a13665
39dc901
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,3 +57,5 @@ lib/*/*.lib | |
lib/*/lib/*.lib | ||
|
||
lib/qtscript-bytearray/*.cc | ||
|
||
*.vscode | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
|
||
#include "broadcast/filelistener.h" | ||
|
||
|
||
FileListener::FileListener(const QString &path) : | ||
m_file(path) | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no new line before { |
||
QFileInfo fileInfo(path); | ||
qDebug() << "Absolute path " << fileInfo.absoluteFilePath(); | ||
qDebug() << "File exists: " << fileInfo.exists(); | ||
m_file.open(QIODevice::WriteOnly | | ||
QIODevice::Text | | ||
QIODevice::Unbuffered); | ||
|
||
} | ||
|
||
void FileListener::broadcastCurrentTrack(TrackPointer pTrack) { | ||
if (!pTrack) | ||
return; | ||
QTextStream stream(&m_file); | ||
m_file.resize(0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A comment helps here: |
||
stream << "Now listening " << pTrack->getTitle(); | ||
stream << " by " << pTrack->getArtist(); | ||
} | ||
|
||
void FileListener::scrobbleTrack(TrackPointer pTrack) { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
#pragma once | ||
|
||
#include <QFile> | ||
#include "broadcast/scrobblingservice.h" | ||
|
||
class FileListener: public ScrobblingService { | ||
public: | ||
FileListener(const QString &path); | ||
void broadcastCurrentTrack(TrackPointer pTrack) override; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inheritance is unnecessary. The file listener could be implemented as a standalone class with 2 slots, connected to the ScrobblingService. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea is to have N services that connect to the scrobbling manager that answer to both slots. If I take one implementation out of the hierarchy then I have to update that separately. It also doesn't allow me to mock up the file listener. I don't see any advantage on breaking the inheritance. Can you elaborate on this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The scrobbling manager can offer 2 public signals for this purpose and dependent services can connect to them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah but who owns the services? I was thinking of getting a list of services from the user settings, pass them to the scobbling manager and then simply keep them updated. I find the manager owning a list of services to be easier to mantain than creating a connection for every service and storing them somewhere else. |
||
void scrobbleTrack(TrackPointer pTrack) override; | ||
private: | ||
QFile m_file; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
#include "metadatabroadcast.h" | ||
#include "mixer/playerinfo.h" | ||
|
||
MetadataBroadcaster::MetadataBroadcaster(TrackTimers::ElapsedTimer *timer) : | ||
m_pElapsedTimer(timer) | ||
{ | ||
|
||
} | ||
|
||
void MetadataBroadcaster::slotReadyToBeScrobbled(TrackPointer pTrack) { | ||
|
||
} | ||
|
||
void MetadataBroadcaster::slotNowListening(TrackPointer pTrack) { | ||
for (auto &service : m_scrobblingServices) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is essentially a manually implemented synchronous pub/sub communication. Why don't you use Qt signal/slots for this purpose with all their thread-safety guarantees? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't use slots because I control when to call the services and I own the metadata broadcast object, which lives in the same thread as the scrobbling manager. Since I know when I will call the services and I don't need queued connections I didn't see any reason to use signals & slots. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Send everything that you need at the receiver side by value, i.e. wrap it into a queueable meta-type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean all the track metadata so the services don't have to store a pointer? Like a DTO? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. const auto& |
||
service->broadcastCurrentTrack(pTrack); | ||
} | ||
} | ||
|
||
QLinkedList<TrackPointer> MetadataBroadcaster::getTrackedTracks() { | ||
return m_trackedTracks; | ||
} | ||
|
||
void MetadataBroadcaster::addNewScrobblingService(ScrobblingService *service) { | ||
m_scrobblingServices.push_back( | ||
std::move(std::unique_ptr<ScrobblingService>(service))); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
#pragma once | ||
|
||
#include <QObject> | ||
#include <QLinkedList> | ||
#include <list> | ||
|
||
#include "broadcast/scrobblingservice.h" | ||
#include "track/track.h" | ||
#include "track/trackplaytimers.h" | ||
|
||
class MetadataBroadcaster : public QObject { | ||
Q_OBJECT | ||
public: | ||
MetadataBroadcaster(TrackTimers::ElapsedTimer *timer); | ||
QLinkedList<TrackPointer> getTrackedTracks(); | ||
void addNewScrobblingService(ScrobblingService *service); | ||
|
||
public slots: | ||
void slotReadyToBeScrobbled(TrackPointer pTrack); | ||
void slotNowListening(TrackPointer pTrack); | ||
private: | ||
QLinkedList<TrackPointer> m_trackedTracks; | ||
std::unique_ptr<TrackTimers::ElapsedTimer> m_pElapsedTimer; | ||
std::list<std::unique_ptr<ScrobblingService>> m_scrobblingServices; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
#include <QObject> | ||
|
||
#include "broadcast/scrobblingmanager.h" | ||
#include "broadcast/filelistener.h" | ||
#include "control/controlproxy.h" | ||
#include "engine/enginexfader.h" | ||
#include "mixer/deck.h" | ||
#include "mixer/playerinfo.h" | ||
#include "mixer/playermanager.h" | ||
|
||
ScrobblingManager::ScrobblingManager(PlayerManager *manager) : | ||
m_CPGuiTick("[Master]", "guiTick50ms",this), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some style nits:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okey, thanks. |
||
m_CPCrossfader("[Master]","crossfader", this), | ||
m_CPXFaderCurve(ConfigKey(EngineXfader::kXfaderConfigKey, | ||
"xFaderCurve"),this), | ||
|
||
m_CPXFaderCalibration(ConfigKey(EngineXfader::kXfaderConfigKey, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't you call the function that calculates the xfader gain instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but I need the correct parameters which are given by those Control objects, aren't they? I copied the code from EngineMaster. |
||
"xFaderCalibration"),this), | ||
|
||
m_CPXFaderMode(ConfigKey(EngineXfader::kXfaderConfigKey, | ||
"xFaderMode"),this), | ||
|
||
m_CPXFaderReverse(ConfigKey(EngineXfader::kXfaderConfigKey, | ||
"xFaderReverse"),this), | ||
|
||
m_pManager(manager), | ||
m_broadcaster(new TrackTimers::ElapsedTimerQt) | ||
{ | ||
m_CPGuiTick.connectValueChanged(SLOT(slotGuiTick(double))); | ||
connect(&PlayerInfo::instance(),SIGNAL(currentPlayingTrackChanged(TrackPointer)), | ||
&m_broadcaster,SLOT(slotNowListening(TrackPointer))); | ||
m_broadcaster | ||
.addNewScrobblingService(new FileListener("nowListening.txt")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is: addNewCurrentTrackConsumer() or so. I think it is better to call this form outside the class. The manager should have no knowledge which type of consumers are connected. It should only track a list of generic consumers if required. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The file should be finally written into a user directory. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's what I was thinking too, but since the user will choose the services this is a WIP solution. The file path is WIP too because the user should be able to choose the path of the file. |
||
startTimer(1000); | ||
} | ||
|
||
|
||
void ScrobblingManager::slotTrackPaused(TrackPointer pPausedTrack) { | ||
QMutexLocker locker(&m_mutex); | ||
bool allPaused = true; | ||
TrackInfo *pausedTrackInfo = nullptr; | ||
for (TrackInfo *trackInfo : m_trackList) { | ||
if (trackInfo->m_pTrack == pPausedTrack) { | ||
pausedTrackInfo = trackInfo; | ||
for (QString playerGroup : trackInfo->m_players) { | ||
BaseTrackPlayer *player = m_pManager->getPlayer(playerGroup); | ||
if (!player->isTrackPaused()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check the pointer before using. If it is always valid ensure it by a VERIFY_OR_DEBUG_ASSERT. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright. |
||
allPaused = false; | ||
} | ||
break; | ||
} | ||
} | ||
if (allPaused && pausedTrackInfo) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use curly braces even for single line blocks with a consistent indentation and continuation style as proposed in our style guide. Of course, we should have an automatic code formatter that enforces the rules and fixes those issues automatically ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okey |
||
pausedTrackInfo->m_trackInfo.pausePlayedTime(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You crash is not a deadlock, it is a Segmentation fault, which happens when you access memory, not allocated by the process. In this case, you access memory via the uninitialized pointer pausedTrackInfo. It is a good idea to always initialize empty pointers with "nullptr". Than you can decorate your code with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow, how can a seg fault keep all threads in a futex? Wouldn't it kill the process? Thanks for the help. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It actually kills the process here on my Ubuntu Trusty. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mybe we have an additional deadlock? |
||
} | ||
|
||
void ScrobblingManager::slotTrackResumed(TrackPointer pResumedTrack) { | ||
BaseTrackPlayer *player = qobject_cast<Deck*>(sender()); | ||
DEBUG_ASSERT(player); | ||
if (player == 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be combined as VERIFY_OR_DEBUG_ASSERT. |
||
qDebug() << "Didn't load track in a deck yet scrobbling " | ||
"received resumed signal."; | ||
return; | ||
} | ||
if (isTrackAudible(pResumedTrack,player)) { | ||
QMutexLocker locker(&m_mutex); | ||
for (TrackInfo *trackInfo : m_trackList) { | ||
if (trackInfo->m_pTrack == pResumedTrack) { | ||
trackInfo->m_trackInfo.resumePlayedTime(); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
void ScrobblingManager::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) { | ||
BaseTrackPlayer *sourcePlayer = | ||
qobject_cast<BaseTrackPlayer*>(sender()); | ||
DEBUG_ASSERT(sourcePlayer); | ||
if (pOldTrack) { | ||
m_tracksToBeReset.append(TrackToBeReset(pOldTrack, | ||
sourcePlayer->getGroup())); | ||
} | ||
} | ||
|
||
void ScrobblingManager::slotNewTrackLoaded(TrackPointer pNewTrack) { | ||
//Empty player gives a null pointer. | ||
if (!pNewTrack) | ||
return; | ||
BaseTrackPlayer *player = qobject_cast<BaseTrackPlayer*>(sender()); | ||
DEBUG_ASSERT(player); | ||
QMutexLocker locker(&m_mutex); | ||
bool trackAlreadyAdded = false; | ||
for (TrackInfo *trackInfo : m_trackList) { | ||
if (trackInfo->m_pTrack == pNewTrack) { | ||
trackInfo->m_players.append(player->getGroup()); | ||
trackAlreadyAdded = true; | ||
break; | ||
} | ||
} | ||
if (!trackAlreadyAdded) { | ||
TrackInfo *newTrackInfo = new TrackInfo(pNewTrack); | ||
newTrackInfo->m_players.append(player->getGroup()); | ||
m_trackList.append(newTrackInfo); | ||
connect(&m_trackList.last()->m_trackInfo,SIGNAL(readyToBeScrobbled(TrackPointer)), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you establish a signal/slot connection between all instances of the internal TrackTimingInfo class and the broadcaster instead of only using a single connection between the ScrobblingManager and the broadcaster? You send the affected track with as the payload data with every signal and the track is bound to each instance of TrackTimingInfo anyway. If you need more context than just the TrackPointer then you could always collect this in a dedicated signal data class. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see how it is any different, you're just adding one level of indirection. In the end, the broadcaster manages the info on the grace period timer, the scrobbling manager doesn't care about it. Could you please explain why you think this is a bad idea? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Simple and robust design? It doesn't make sense to use n connections when 1 is sufficient and all the receiver needs to know about the context is already part of the message. The track info objects are an implementation detail of your class that should not be visible from the outside. The connections make them visible, indirectly. You are also not allowed to delete those objects like you do now while some signals might still be pending. You have to delete them with QObject::deleteLater(). Otherwise the receiver will get a dangling sender pointer. But all this doesn't matter if you lift the connection up to the next level. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct me if I'm wrong but if they live on the same thread aren't they under a direct connection? Then there are no pending signals because the slot is called immediately. However, I didn't think about exposing implementation details so you're right, I will call them from the scrobbling manager. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A direct connection is finally a simple callback, Unfortunately, Qt has a lot of boilerplate code to detect that and to make connect and reconnect save in all circumstances. |
||
&m_broadcaster,SLOT(slotReadyToBeScrobbled(TrackPointer))); | ||
} | ||
//A new track has been loaded so must unload old one. | ||
resetTracks(); | ||
} | ||
|
||
void ScrobblingManager::slotPlayerEmpty() { | ||
QMutexLocker locker(&m_mutex); | ||
resetTracks(); | ||
} | ||
|
||
void ScrobblingManager::resetTracks() { | ||
for (TrackToBeReset candidateTrack : m_tracksToBeReset) { | ||
QLinkedList<TrackInfo*>::Iterator trackListIterator = | ||
m_trackList.begin(); | ||
for (;trackListIterator != m_trackList.end(); ++trackListIterator) { | ||
TrackInfo *trackInfo = *trackListIterator; | ||
if (trackInfo->m_pTrack == candidateTrack.m_pTrack) { | ||
if (!trackInfo->m_players.contains(candidateTrack. | ||
m_playerGroup)) { | ||
qDebug() << "Track doesn't contain player" | ||
"yet is requested for deletion."; | ||
break; | ||
} | ||
//Load error, stray from engine buffer. | ||
BaseTrackPlayer *player = | ||
m_pManager->getPlayer(candidateTrack.m_playerGroup); | ||
if (player->getLoadedTrack() == | ||
candidateTrack.m_pTrack) | ||
break; | ||
QLinkedList<QString>::iterator it = | ||
trackInfo->m_players.begin(); | ||
while (it != trackInfo->m_players.end() && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3 nested loops with lots of conditional statements feels way too heavy for the task of synchronizing internal state. Please rethink this piece of code. It also contains the sensitive memory management as I mentioned out elsewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is inefficient, yes, but the number of tracks is so low I haven't seen any performance hits. I didn't want to optimize unless I saw any so I think the code is fine this way, there will be at most 8 different tracks between both lists. Also I don' t understand what you mean with the sensitive memory management. I delete a pointer I created in the slotNewTrack. Since I know where the pointer is coming from I don't see any problem with it. I can wrap it in a unique_ptr though if you think it's better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not complaining about efficiency, I'm talking about clean and readable code ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If its not about efficiency, how would you go about iterating through the list of tracks to be reset, picking each track to be reset, looking for it on the list of current tracks and then deleting only the player that's been ejected from the list of players each tracked track has without 3 loops? I'm open to suggestions. |
||
*it != candidateTrack.m_playerGroup) { | ||
++it; | ||
} | ||
if (*it == candidateTrack.m_playerGroup) { | ||
trackInfo->m_players.erase(it); | ||
} | ||
if (trackInfo->m_players.empty()) { | ||
trackInfo->m_trackInfo.pausePlayedTime(); | ||
trackInfo->m_trackInfo.resetPlayedTime(); | ||
delete trackInfo; | ||
m_trackList.erase(trackListIterator); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
bool ScrobblingManager::isTrackAudible(TrackPointer pTrack, BaseTrackPlayer * pPlayer) { | ||
if (pPlayer->getLoadedTrack() != pTrack) { | ||
qDebug() << "Track can't be audible because is not in player"; | ||
return false; | ||
} | ||
return getPlayerVolume(pPlayer) >= 0.20; | ||
} | ||
|
||
double ScrobblingManager::getPlayerVolume(BaseTrackPlayer *pPlayer) { | ||
double finalVolume; | ||
ControlProxy trackPreGain(pPlayer->getGroup(),"pregain",this); | ||
double preGain = trackPreGain.get(); | ||
ControlProxy trackVolume(pPlayer->getGroup(),"volume",this); | ||
double volume = trackVolume.get(); | ||
ControlProxy deckOrientation(pPlayer->getGroup(),"orientation",this); | ||
int orientation = deckOrientation.get(); | ||
|
||
double xFaderLeft,xFaderRight; | ||
|
||
EngineXfader::getXfadeGains(m_CPCrossfader.get(), | ||
m_CPXFaderCurve.get(), | ||
m_CPXFaderCalibration.get(), | ||
m_CPXFaderMode.get(), | ||
m_CPXFaderReverse.toBool(), | ||
&xFaderLeft,&xFaderRight); | ||
finalVolume = preGain * volume; | ||
if (orientation == EngineChannel::LEFT) | ||
finalVolume *= xFaderLeft; | ||
else if (orientation == EngineChannel::RIGHT) | ||
finalVolume *= xFaderRight; | ||
return finalVolume; | ||
} | ||
|
||
void ScrobblingManager::slotGuiTick(double timeSinceLastTick) { | ||
for (TrackInfo *trackInfo : m_trackList) { | ||
trackInfo->m_trackInfo.slotGuiTick(timeSinceLastTick); | ||
} | ||
} | ||
|
||
void ScrobblingManager::timerEvent(QTimerEvent *timerEvent) { | ||
for (TrackInfo *trackInfo : m_trackList) { | ||
bool inaudible = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please try to avoid naming a boolean variables with a negation names. The name "audible" would work much better here. But the local variable is not needed anyway, the code can be simplified. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll change the name of the variable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need the variable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you elaborate? |
||
for (QString playerGroup : trackInfo->m_players) { | ||
BaseTrackPlayer *player = m_pManager->getPlayer(playerGroup); | ||
if (isTrackAudible(trackInfo->m_pTrack,player)) { | ||
inaudible = false; | ||
break; | ||
} | ||
} | ||
if (inaudible) { | ||
trackInfo->m_trackInfo.pausePlayedTime(); | ||
} | ||
} | ||
} |
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 file is not created by Mixxx or its build system. Adding patterns for every conceivable editor/IDE to this project's gitignore is neither desirable nor feasible. You should add it to your global gitignore 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.
I agree, we should stop adding these editor generated files to our shared .gitignore and remove the ones that are already there.
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 was already mentioned in that PR ages ago. I can't seem to find it.