Compare commits

..

16 Commits

Author SHA1 Message Date
Jonas Kvinge
c658a77b05 Release 1.2.13 2025-08-31 22:33:48 +02:00
Jonas Kvinge
1880dc8153 Update Changelog 2025-08-31 22:27:00 +02:00
7xnl
b5fd3d5717 Add settings customize Discord status text
The new settings let you customize the "Listening to" status text, according to the [status display types](https://discord.com/developers/docs/events/gateway-events#activity-object).

Fixes #1796.
2025-08-31 22:11:59 +02:00
Jonas Kvinge
3c3480fb84 SystemTrayIcon: Respect device aspect ratio
Fixes #1782
2025-08-31 02:34:13 +02:00
Jonas Kvinge
f628914173 MainWindow: Rename systemtrayicon 2025-08-31 00:37:09 +02:00
Jonas Kvinge
c100fb1bb8 TagReaderTagLib: Fallback to "Other" cover type
Fixes #1793
2025-08-31 00:20:00 +02:00
Jonas Kvinge
8c804c4fba Refactor CDDA loading signal/slots
Fixes #1803
2025-08-31 00:01:55 +02:00
Jonas Kvinge
912a7c7da9 MusicBrainzClient: Fix typo 2025-08-30 23:55:27 +02:00
Jonas Kvinge
0a5815c82e StyleSheetLoader: Set alpha on other platforms than macOS
Fixes #1806
2025-08-26 22:48:58 +02:00
Jonas Kvinge
6513b3032b CMake: Check additional names for getopt 2025-08-24 22:36:13 +02:00
Jonas Kvinge
8c51401bdc MacOsDeviceLister: Fix build without MTP
Fixes #1804
2025-08-24 01:28:22 +02:00
dependabot[bot]
45fc9c83d4 Bump vmactions/openbsd-vm from 1.1.8 to 1.2.0
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.1.8 to 1.2.0.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.1.8...v1.2.0)

---
updated-dependencies:
- dependency-name: vmactions/openbsd-vm
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-20 23:57:47 +02:00
dependabot[bot]
be57d8147a Bump vmactions/freebsd-vm from 1.2.1 to 1.2.3
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.2.1 to 1.2.3.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.2.1...v1.2.3)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-19 18:25:13 +02:00
Jonas Kvinge
a97908fb6b CI: Bump msvc sdk 2025-08-17 13:44:57 +02:00
Lars Wendler
c0417d4bb3 cdda: fix build without musicbrainz
With -DENABLE_MUSICBRAINZ=NO the following build error occurs since 1.2.12:

/var/tmp/portage/media-sound/strawberry-1.2.12_pre/work/strawberry-1.2.12/src/de
vice/cddasongloader.cpp:58:91: error: ‘LoadMusicBrainzCDTags’ is not a member of
 ‘CDDASongLoader’
   58 |   QObject::connect(this, &CDDASongLoader::MusicBrainzDiscIdLoaded, this,
 &CDDASongLoader::LoadMusicBrainzCDTags);
      |
                  ^~~~~~~~~~~~~~~~~~~~~
2025-08-13 19:49:41 +02:00
Jonas Kvinge
062e2cfb84 Turn on git revision 2025-08-13 00:20:05 +02:00
31 changed files with 561 additions and 258 deletions

View File

@@ -733,7 +733,7 @@ jobs:
submodules: recursive
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.2.1
uses: vmactions/freebsd-vm@v1.2.3
with:
usesh: true
mem: 4096
@@ -758,7 +758,7 @@ jobs:
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.1.8
uses: vmactions/openbsd-vm@v1.2.0
with:
usesh: true
mem: 4096
@@ -1295,6 +1295,20 @@ jobs:
shell: bash
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
- name: Show SDK versions
shell: bash
run: ls -la "c:/Program Files (x86)/Windows Kits/10/include"
- name: Set SDK version
if: matrix.arch != 'arm64'
shell: bash
run: echo "sdk_version=10.0.19041.0" >> $GITHUB_ENV
- name: Set SDK version
if: matrix.arch == 'arm64'
shell: bash
run: echo "sdk_version=10.0.26100.0" >> $GITHUB_ENV
- name: Install rsync
shell: cmd
run: choco install --no-progress rsync
@@ -1377,7 +1391,7 @@ jobs:
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{matrix.arch}}
sdk: 10.0.20348.0
sdk: ${{env.sdk_version}}
vsversion: 2022
- name: Checkout

View File

@@ -32,6 +32,7 @@ extern "C" {
typedef struct DiscordRichPresence {
int type;
int status_display_type;
const char *name; /* max 128 bytes */
const char *state; /* max 128 bytes */
const char *details; /* max 128 bytes */

View File

@@ -128,6 +128,9 @@ size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce
if (presence->type >= 0 && presence->type <= 5) {
WriteKey(writer, "type");
writer.Int(presence->type);
WriteKey(writer, "status_display_type");
writer.Int(presence->status_display_type);
}
WriteOptionalString(writer, "name", presence->name);

View File

@@ -259,7 +259,16 @@ if(APPLE)
endif()
if(WIN32)
find_package(getopt-win REQUIRED)
find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
if(TARGET getopt::getopt)
set(GETOPT_LIBRARIES getopt::getopt)
elseif(TARGET getopt-win::getopt)
set(GETOPT_LIBRARIES getopt-win::getopt)
elseif(TARGET getopt::getopt_shared)
set(GETOPT_LIBRARIES getopt::getopt_shared)
else()
message(FATAL_ERROR "Missing getopt")
endif()
endif()
if(APPLE OR WIN32)
@@ -1554,7 +1563,7 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt>
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
$<$<BOOL:${MSVC}>:WindowsApp>
KDAB::kdsingleapplication
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>

View File

@@ -2,6 +2,19 @@ Strawberry Music Player
=======================
ChangeLog
Version 1.2.13 (2025.08.31):
Bugfixes:
* Fixed playlist alternating row colors no longer working with some styles (#1806)
* Fixed "Open Audio CD" no longer working (#1803)
* Fixed systemtray icon playback status not working with scaling (#1782)
* Fixed build without MusicBrainz (#1799)
* Fixed build without MTP (#1804)
Enhancements:
* Added Discord status text option (#1796)
* Read Vorbis/FLAC "Other" embedded covers if front cover is not available (#1793)
Version 1.2.12 (2025.08.12):
Bugfixes:

View File

@@ -1,6 +1,6 @@
set(STRAWBERRY_VERSION_MAJOR 1)
set(STRAWBERRY_VERSION_MINOR 2)
set(STRAWBERRY_VERSION_PATCH 12)
set(STRAWBERRY_VERSION_PATCH 13)
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION OFF)

View File

@@ -51,6 +51,7 @@
</screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact>
<releases>
<release version="1.2.13" date="2025-08-31"/>
<release version="1.2.12" date="2025-08-12"/>
<release version="1.2.11" date="2025-05-15"/>
<release version="1.2.10" date="2025-04-18"/>

View File

@@ -71,6 +71,14 @@ constexpr char kSettingsGroup[] = "DiscordRPC";
constexpr char kEnabled[] = "enabled";
constexpr char kStatusDisplayType[] = "StatusDisplayType";
enum class StatusDisplayType {
App = 0,
Artist,
Song
};
} // namespace
#endif // NOTIFICATIONSSETTINGS_H

View File

@@ -279,7 +279,7 @@ constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-
#endif // HAVE_QTSPARKLE
MainWindow::MainWindow(Application *app,
SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd,
SharedPtr<SystemTrayIcon> systemtrayicon, OSDBase *osd,
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence,
#endif
@@ -291,7 +291,7 @@ MainWindow::MainWindow(Application *app,
thumbbar_(new Windows7ThumbBar(this)),
#endif
app_(app),
tray_icon_(tray_icon),
systemtrayicon_(systemtrayicon),
osd_(osd),
#ifdef HAVE_DISCORD_RPC
discord_rich_presence_(discord_rich_presence),
@@ -408,6 +408,8 @@ MainWindow::MainWindow(Application *app,
setWindowIcon(IconLoader::Load(u"strawberry"_s));
}
systemtrayicon_->SetDevicePixelRatioF(devicePixelRatioF());
QObject::connect(&*app->database(), &Database::Error, this, &MainWindow::ShowErrorDialog);
album_cover_choice_controller_->Init(app->network(), app->tagreader_client(), app->collection()->backend(), app->albumcover_loader(), app->current_albumcover_loader(), app->cover_providers(), app->streaming_services());
@@ -846,14 +848,14 @@ MainWindow::MainWindow(Application *app,
mac::SetApplicationHandler(this);
#endif
// Tray icon
tray_icon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit);
QObject::connect(&*tray_icon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
QObject::connect(&*tray_icon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
QObject::connect(&*tray_icon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
QObject::connect(&*tray_icon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
QObject::connect(&*tray_icon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
systemtrayicon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
// Windows 7 thumbbar buttons
#ifdef Q_OS_WIN32
@@ -1031,7 +1033,7 @@ MainWindow::MainWindow(Application *app,
show();
break;
case BehaviourSettings::StartupBehaviour::Hide:
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) {
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible()) {
break;
}
[[fallthrough]];
@@ -1044,7 +1046,7 @@ MainWindow::MainWindow(Application *app,
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized);
if (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
if (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
show();
}
break;
@@ -1156,13 +1158,13 @@ void MainWindow::ReloadSettings() {
#ifdef Q_OS_MACOS
constexpr bool keeprunning_available = true;
#else
const bool systemtray_available = tray_icon_->IsSystemTrayAvailable();
const bool systemtray_available = systemtrayicon_->IsSystemTrayAvailable();
s.beginGroup(BehaviourSettings::kSettingsGroup);
const bool showtrayicon = s.value(BehaviourSettings::kShowTrayIcon, systemtray_available).toBool();
s.endGroup();
const bool keeprunning_available = systemtray_available && showtrayicon;
if (systemtray_available) {
tray_icon_->setVisible(showtrayicon);
systemtrayicon_->setVisible(showtrayicon);
}
if ((!showtrayicon || !systemtray_available) && !isVisible()) {
show();
@@ -1187,7 +1189,7 @@ void MainWindow::ReloadSettings() {
int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt();
s.endGroup();
tray_icon_->SetTrayiconProgress(trayicon_progress);
systemtrayicon_->SetTrayiconProgress(trayicon_progress);
#ifdef HAVE_DBUS
if (taskbar_progress_ && !taskbar_progress) {
@@ -1209,11 +1211,11 @@ void MainWindow::ReloadSettings() {
ui_->volume->SetEnabled(volume_control);
if (volume_control) {
if (!ui_->action_mute->isVisible()) ui_->action_mute->setVisible(true);
if (!tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(true);
if (!systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(true);
}
else {
if (ui_->action_mute->isVisible()) ui_->action_mute->setVisible(false);
if (tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(false);
if (systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(false);
}
}
@@ -1365,8 +1367,8 @@ void MainWindow::Exit() {
if (app_->player()->GetState() == EngineBase::State::Playing) {
app_->player()->Stop();
hide();
if (tray_icon_->IsSystemTrayAvailable()) {
tray_icon_->setVisible(false);
if (systemtrayicon_->IsSystemTrayAvailable()) {
systemtrayicon_->setVisible(false);
}
return; // Don't quit the application now: wait for the fadeout finished signal
}
@@ -1423,7 +1425,7 @@ void MainWindow::MediaStopped() {
ui_->action_love->setEnabled(false);
ui_->button_love->setEnabled(false);
tray_icon_->LoveStateChanged(false);
systemtrayicon_->LoveStateChanged(false);
if (track_position_timer_->isActive()) {
track_position_timer_->stop();
@@ -1432,8 +1434,8 @@ void MainWindow::MediaStopped() {
track_slider_timer_->stop();
}
ui_->track_slider->SetStopped();
tray_icon_->SetProgress(0);
tray_icon_->SetStopped();
systemtrayicon_->SetProgress(0);
systemtrayicon_->SetStopped();
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -1465,7 +1467,7 @@ void MainWindow::MediaPaused() {
track_slider_timer_->start();
}
tray_icon_->SetPaused();
systemtrayicon_->SetPaused();
}
@@ -1486,7 +1488,7 @@ void MainWindow::MediaPlaying() {
}
ui_->action_play_pause->setEnabled(enable_play_pause);
ui_->track_slider->SetCanSeek(can_seek);
tray_icon_->SetPlaying(enable_play_pause);
systemtrayicon_->SetPlaying(enable_play_pause);
if (!track_position_timer_->isActive()) {
track_position_timer_->start();
@@ -1507,14 +1509,14 @@ void MainWindow::SendNowPlaying() {
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->EffectiveMetadata());
ui_->action_love->setEnabled(true);
ui_->button_love->setEnabled(true);
tray_icon_->LoveStateChanged(true);
systemtrayicon_->LoveStateChanged(true);
}
}
void MainWindow::VolumeChanged(const uint volume) {
ui_->action_mute->setChecked(volume == 0);
tray_icon_->MuteButtonStateChanged(volume == 0);
systemtrayicon_->MuteButtonStateChanged(volume == 0);
}
void MainWindow::SongChanged(const Song &song) {
@@ -1524,7 +1526,7 @@ void MainWindow::SongChanged(const Song &song) {
song_playing_ = song;
song_ = song;
setWindowTitle(song.PrettyTitleWithArtist());
tray_icon_->SetProgress(0);
systemtrayicon_->SetProgress(0);
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -1707,7 +1709,7 @@ void MainWindow::hideEvent(QHideEvent *e) {
void MainWindow::closeEvent(QCloseEvent *e) {
if (!exit_ && (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !keep_running_)) {
if (!exit_ && (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !keep_running_)) {
Exit();
}
@@ -1718,7 +1720,7 @@ void MainWindow::closeEvent(QCloseEvent *e) {
void MainWindow::SetHiddenInTray(const bool hidden) {
if (hidden && isVisible()) {
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible() && keep_running_) {
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible() && keep_running_) {
close();
}
else {
@@ -1747,7 +1749,7 @@ void MainWindow::Seeked(const qint64 microseconds) {
const qint64 position = microseconds / kUsecPerSec;
const qint64 length = app_->player()->GetCurrentItem()->EffectiveMetadata().length_nanosec() / kNsecPerSec;
tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -1767,7 +1769,7 @@ void MainWindow::UpdateTrackPosition() {
const int position = std::floor(static_cast<float>(app_->player()->engine()->position_nanosec()) / static_cast<float>(kNsecPerSec) + 0.5);
// Update the tray icon every 10 seconds
if (position % 10 == 0) tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
if (position % 10 == 0) systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -3259,7 +3261,7 @@ void MainWindow::LoveButtonVisibilityChanged(const bool value) {
else
ui_->widget_love->hide();
tray_icon_->LoveVisibilityChanged(value);
systemtrayicon_->LoveVisibilityChanged(value);
}
@@ -3282,7 +3284,7 @@ void MainWindow::Love() {
app_->scrobbler()->Love();
ui_->button_love->setEnabled(false);
ui_->action_love->setEnabled(false);
tray_icon_->LoveStateChanged(false);
systemtrayicon_->LoveStateChanged(false);
}

View File

@@ -111,7 +111,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
public:
explicit MainWindow(Application *app,
SharedPtr<SystemTrayIcon> tray_icon,
SharedPtr<SystemTrayIcon> systemtrayicon,
OSDBase *osd,
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence,
@@ -310,7 +310,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
#endif
Application *app_;
SharedPtr<SystemTrayIcon> tray_icon_;
SharedPtr<SystemTrayIcon> systemtrayicon_;
OSDBase *osd_;
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence_;

View File

@@ -179,8 +179,10 @@ SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD
CDDASongLoader *cdda_song_loader = new CDDASongLoader(QUrl(), this);
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot);
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded);
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadError, this, &SongLoader::AudioCDTracksLoadErrorSlot);
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsLoaded, this, &SongLoader::AudioCDTracksLoadedSlot);
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsUpdated, this, &SongLoader::AudioCDTracksUpdatedSlot);
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadingFinished, this, &SongLoader::AudioCDLoadingFinishedSlot);
cdda_song_loader->LoadSongs();
return Result::Success;
#else
@@ -192,23 +194,38 @@ SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD
void SongLoader::AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error) {
void SongLoader::AudioCDTracksLoadErrorSlot(const QString &error) {
songs_ = songs;
errors_ << error;
Q_EMIT AudioCDTracksLoadFinished();
}
void SongLoader::AudioCDTracksTagsLoaded(const SongList &songs) {
void SongLoader::AudioCDTracksLoadedSlot(const SongList &songs) {
songs_ = songs;
Q_EMIT AudioCDTracksLoaded();
}
void SongLoader::AudioCDTracksUpdatedSlot(const SongList &songs) {
songs_ = songs;
Q_EMIT AudioCDTracksUpdated();
}
void SongLoader::AudioCDLoadingFinishedSlot() {
CDDASongLoader *cdda_song_loader = qobject_cast<CDDASongLoader*>(sender());
cdda_song_loader->deleteLater();
songs_ = songs;
Q_EMIT LoadAudioCDFinished(true);
Q_EMIT AudioCDLoadingFinished(true);
}
#endif
#endif // HAVE_AUDIOCD
SongLoader::Result SongLoader::LoadLocal(const QString &filename) {

View File

@@ -90,17 +90,21 @@ class SongLoader : public QObject {
QStringList errors() { return errors_; }
Q_SIGNALS:
void AudioCDTracksLoadFinished();
void LoadAudioCDFinished(const bool success);
void AudioCDTracksLoaded();
void AudioCDTracksUpdated();
void AudioCDLoadingFinished(const bool success);
void LoadRemoteFinished();
private Q_SLOTS:
void ScheduleTimeout();
void Timeout();
void StopTypefind();
#ifdef HAVE_AUDIOCD
void AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error);
void AudioCDTracksTagsLoaded(const SongList &songs);
void AudioCDTracksLoadErrorSlot(const QString &error);
void AudioCDTracksLoadedSlot(const SongList &songs);
void AudioCDTracksUpdatedSlot(const SongList &songs);
void AudioCDLoadingFinishedSlot();
#endif // HAVE_AUDIOCD
private:

View File

@@ -32,6 +32,7 @@
#include <QFile>
#include <QString>
#include <QPalette>
#include <QColor>
#include <QEvent>
#include "includes/shared_ptr.h"
@@ -79,13 +80,13 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, SharedPtr<StyleSheetDat
// Replace %palette-role with actual colours
QPalette p(widget->palette());
{
QColor color_altbase = p.color(QPalette::AlternateBase);
QColor color_altbase = p.color(QPalette::AlternateBase);
#ifdef Q_OS_MACOS
color_altbase.setAlpha(color_altbase.lightness() > 180 ? 130 : 16);
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? (color_altbase.lightness() > 180 ? 130 : 16) : color_altbase.alpha());
#else
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? 116 : color_altbase.alpha());
#endif
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(color_altbase.red()).arg(color_altbase.green()).arg(color_altbase.blue()).arg(color_altbase.alpha()));
}
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(color_altbase.red()).arg(color_altbase.green()).arg(color_altbase.blue()).arg(color_altbase.alpha()));
ReplaceColor(&stylesheet, u"Window"_s, p, QPalette::Window);
ReplaceColor(&stylesheet, u"Background"_s, p, QPalette::Window);

View File

@@ -63,9 +63,8 @@ CDDADevice::CDDADevice(const QUrl &url,
timer_disc_changed_->setInterval(1s);
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsLoaded, this, &CDDADevice::SongsLoaded);
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsDurationLoaded, this, &CDDADevice::SongsLoaded);
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsMetadataLoaded, this, &CDDADevice::SongsLoaded);
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongLoadingFinished, this, &CDDADevice::SongLoadingFinished);
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsUpdated, this, &CDDADevice::SongsLoaded);
QObject::connect(&cdda_song_loader_, &CDDASongLoader::LoadingFinished, this, &CDDADevice::SongLoadingFinished);
QObject::connect(this, &CDDADevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate);
QObject::connect(timer_disc_changed_, &QTimer::timeout, this, &CDDADevice::CheckDiscChanged);

View File

@@ -55,8 +55,9 @@ CDDASongLoader::CDDASongLoader(const QUrl &url, QObject *parent)
url_(url),
network_(make_shared<NetworkAccessManager>()) {
QObject::connect(this, &CDDASongLoader::MusicBrainzDiscIdLoaded, this, &CDDASongLoader::LoadMusicBrainzCDTags);
#ifdef HAVE_MUSICBRAINZ
QObject::connect(this, &CDDASongLoader::LoadTagsFromMusicBrainz, this, &CDDASongLoader::LoadTagsFromMusicBrainzSlot);
#endif // HAVE_MUSICBRAINZ
}
CDDASongLoader::~CDDASongLoader() {
@@ -93,7 +94,7 @@ void CDDASongLoader::LoadSongsFromCDDA() {
Error(QStringLiteral("%1: %2").arg(error->code).arg(QString::fromUtf8(error->message)));
}
if (!cdda) {
Q_EMIT SongLoadingFinished();
Error(tr("Could not create cdiocddasrc"));
return;
}
@@ -110,7 +111,6 @@ void CDDASongLoader::LoadSongsFromCDDA() {
gst_object_unref(GST_OBJECT(cdda));
cdda = nullptr;
Error(tr("Error while setting CDDA device to ready state."));
Q_EMIT SongLoadingFinished();
return;
}
@@ -119,7 +119,6 @@ void CDDASongLoader::LoadSongsFromCDDA() {
gst_object_unref(GST_OBJECT(cdda));
cdda = nullptr;
Error(tr("Error while setting CDDA device to pause state."));
Q_EMIT SongLoadingFinished();
return;
}
@@ -132,7 +131,6 @@ void CDDASongLoader::LoadSongsFromCDDA() {
gst_object_unref(GST_OBJECT(cdda));
cdda = nullptr;
Error(tr("Error while querying CDDA tracks."));
Q_EMIT SongLoadingFinished();
return;
}
@@ -142,7 +140,6 @@ void CDDASongLoader::LoadSongsFromCDDA() {
gst_object_unref(GST_OBJECT(cdda));
cdda = nullptr;
Error(tr("Error while querying CDDA tracks."));
Q_EMIT SongLoadingFinished();
return;
}
@@ -157,9 +154,12 @@ void CDDASongLoader::LoadSongsFromCDDA() {
song.set_track(track_number);
songs.insert(track_number, song);
}
Q_EMIT SongsLoaded(songs.values());
#ifdef HAVE_MUSICBRAINZ
gst_tag_register_musicbrainz_tags();
#endif // HAVE_MUSICBRAINZ
GstElement *pipeline = gst_pipeline_new("pipeline");
GstElement *sink = gst_element_factory_make("fakesink", nullptr);
@@ -172,7 +172,9 @@ void CDDASongLoader::LoadSongsFromCDDA() {
int track_artist_tags = 0;
int track_album_tags = 0;
int track_title_tags = 0;
#ifdef HAVE_MUSICBRAINZ
QString musicbrainz_discid;
#endif // HAVE_MUSICBRAINZ
GstMessageType msg_filter = static_cast<GstMessageType>(GST_MESSAGE_TOC|GST_MESSAGE_TAG);
while (msg_filter != 0 && (msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND * 5, msg_filter))) {
@@ -334,41 +336,50 @@ void CDDASongLoader::LoadSongsFromCDDA() {
// This will also cause cdda to be unref'd.
gst_object_unref(pipeline);
Q_EMIT SongsMetadataLoaded(songs.values());
if ((track_artist_tags >= total_tracks && track_album_tags >= total_tracks && track_title_tags >= total_tracks)) {
qLog(Info) << "Songs loaded from CD-Text";
Q_EMIT SongLoadingFinished();
Q_EMIT SongsUpdated(songs.values());
Q_EMIT LoadingFinished();
}
else {
#ifdef HAVE_MUSICBRAINZ
if (musicbrainz_discid.isEmpty()) {
qLog(Info) << "CD is missing tags";
Q_EMIT LoadingFinished();
}
else {
qLog(Info) << "MusicBrainz Disc ID:" << musicbrainz_discid;
Q_EMIT MusicBrainzDiscIdLoaded(musicbrainz_discid);
Q_EMIT LoadTagsFromMusicBrainz(musicbrainz_discid);
}
#else
Q_EMIT LoadingFinished();
#endif // HAVE_MUSICBRAINZ
}
}
#ifdef HAVE_MUSICBRAINZ
void CDDASongLoader::LoadMusicBrainzCDTags(const QString &musicbrainz_discid) const {
void CDDASongLoader::LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid) const {
MusicBrainzClient *musicbrainz_client = new MusicBrainzClient(network_);
QObject::connect(musicbrainz_client, &MusicBrainzClient::DiscIdFinished, this, &CDDASongLoader::MusicBrainzCDTagsLoaded);
QObject::connect(musicbrainz_client, &MusicBrainzClient::DiscIdFinished, this, &CDDASongLoader::LoadTagsFromMusicBrainzFinished);
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
}
void CDDASongLoader::MusicBrainzCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results) {
void CDDASongLoader::LoadTagsFromMusicBrainzFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results, const QString &error) {
MusicBrainzClient *musicbrainz_client = qobject_cast<MusicBrainzClient*>(sender());
musicbrainz_client->deleteLater();
if (!error.isEmpty()) {
Error(error);
return;
}
if (results.empty()) {
Q_EMIT SongLoadingFinished();
Q_EMIT LoadingFinished();
return;
}
@@ -392,8 +403,8 @@ void CDDASongLoader::MusicBrainzCDTagsLoaded(const QString &artist, const QStrin
songs << song;
}
Q_EMIT SongsMetadataLoaded(songs);
Q_EMIT SongLoadingFinished();
Q_EMIT SongsUpdated(songs);
Q_EMIT LoadingFinished();
}
@@ -402,6 +413,8 @@ void CDDASongLoader::MusicBrainzCDTagsLoaded(const QString &artist, const QStrin
void CDDASongLoader::Error(const QString &error) {
qLog(Error) << error;
Q_EMIT SongsDurationLoaded(SongList(), error);
Q_EMIT LoadError(error);
Q_EMIT LoadingFinished();
}

View File

@@ -58,17 +58,16 @@ class CDDASongLoader : public QObject {
QUrl GetUrlFromTrack(const int track_number) const;
Q_SIGNALS:
void SongsLoadError(const QString &error);
void SongsLoaded(const SongList &songs);
void SongLoadingFinished();
void SongsDurationLoaded(const SongList &songs, const QString &error = QString());
void SongsMetadataLoaded(const SongList &songs);
void MusicBrainzDiscIdLoaded(const QString &musicbrainz_discid);
void SongsUpdated(const SongList &songs);
void LoadError(const QString &error);
void LoadingFinished();
void LoadTagsFromMusicBrainz(const QString &musicbrainz_discid);
private Q_SLOTS:
#ifdef HAVE_MUSICBRAINZ
void LoadMusicBrainzCDTags(const QString &musicbrainz_discid) const;
void MusicBrainzCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results);
void LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid) const;
void LoadTagsFromMusicBrainzFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results, const QString &error);
#endif
private:

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -60,6 +60,7 @@ class MacOsDeviceLister : public DeviceLister {
void UpdateDeviceFreeSpace(const QString &id);
#ifdef HAVE_MTP
struct MTPDevice {
MTPDevice() : capacity(0), free_space(0) {}
QString vendor;
@@ -74,6 +75,7 @@ class MacOsDeviceLister : public DeviceLister {
quint64 capacity;
quint64 free_space;
};
#endif // HAVE_MTP
void ExitAsync();
@@ -91,11 +93,12 @@ class MacOsDeviceLister : public DeviceLister {
static void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context);
void FoundMTPDevice(const MTPDevice &device, const QString &serial);
#ifdef HAVE_MTP
void FoundMTPDevice(const MTPDevice &mtp_device, const QString &serial);
void RemovedMTPDevice(const QString &serial);
quint64 GetFreeSpace(const QUrl &url);
quint64 GetCapacity(const QUrl &url);
#endif // HAVE_MTP
bool IsCDDevice(const QString &serial) const;
@@ -103,18 +106,23 @@ class MacOsDeviceLister : public DeviceLister {
CFRunLoopRef run_loop_;
QMap<QString, QString> current_devices_;
#ifdef HAVE_MTP
QMap<QString, MTPDevice> mtp_devices_;
#endif
QSet<QString> cd_devices_;
#ifdef HAVE_MTP
QMutex libmtp_mutex_;
static QSet<MTPDevice> sMTPDeviceList;
#endif
};
size_t qHash(const MacOsDeviceLister::MTPDevice &device);
#ifdef HAVE_MTP
size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device);
inline bool operator==(const MacOsDeviceLister::MTPDevice &a, const MacOsDeviceLister::MTPDevice &b) {
return (a.vendor_id == b.vendor_id) && (a.product_id == b.product_id);
}
#endif // HAVE_MTP
#endif // MACDEVICELISTER_H

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,7 +21,9 @@
#include "config.h"
#include <libmtp.h>
#ifdef HAVE_MTP
# include <libmtp.h>
#endif
#include <AvailabilityMacros.h>
#include <CoreFoundation/CFRunLoop.h>
@@ -41,12 +43,15 @@
#include <QScopeGuard>
#include "macosdevicelister.h"
#include "mtpconnection.h"
#include "includes/scoped_cftyperef.h"
#include "includes/scoped_nsobject.h"
#include "core/logging.h"
#include "core/scoped_nsautorelease_pool.h"
#ifdef HAVE_MTP
# include "mtpconnection.h"
#endif
#import <AppKit/NSWorkspace.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSNotification.h>
@@ -102,11 +107,15 @@ class ScopedIOObject {
// Libgphoto2 MTP detection code:
// http://www.sfr-fresh.com/unix/privat/libgphoto2-2.4.10.1.tar.gz:a/libgphoto2-2.4.10.1/libgphoto2_port/usb/check-mtp-device.c
#ifdef HAVE_MTP
QSet<MacOsDeviceLister::MTPDevice> MacOsDeviceLister::sMTPDeviceList;
#endif
size_t qHash(const MacOsDeviceLister::MTPDevice &d) {
return qHash(d.vendor_id) ^ qHash(d.product_id);
#ifdef HAVE_MTP
size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device) {
return qHash(mtp_device.vendor_id) ^ qHash(mtp_device.product_id);
}
#endif
MacOsDeviceLister::MacOsDeviceLister(QObject *parent) : DeviceLister(parent) {}
@@ -116,6 +125,7 @@ bool MacOsDeviceLister::Init() {
ScopedNSAutoreleasePool pool;
#ifdef HAVE_MTP
// Populate MTP Device list.
if (sMTPDeviceList.empty()) {
LIBMTP_device_entry_t *devices = nullptr;
@@ -126,25 +136,26 @@ bool MacOsDeviceLister::Init() {
else {
for (int i = 0; i < num; ++i) {
LIBMTP_device_entry_t device = devices[i];
MTPDevice d;
d.vendor = QString::fromLatin1(device.vendor);
d.vendor_id = device.vendor_id;
d.product = QString::fromLatin1(device.product);
d.product_id = device.product_id;
d.quirks = device.device_flags;
sMTPDeviceList << d;
MTPDevice mtp_device;
mtp_device.vendor = QString::fromLatin1(device.vendor);
mtp_device.vendor_id = device.vendor_id;
mtp_device.product = QString::fromLatin1(device.product);
mtp_device.product_id = device.product_id;
mtp_device.quirks = device.device_flags;
sMTPDeviceList << mtp_device;
}
}
MTPDevice d;
d.vendor = "SanDisk"_L1;
d.vendor_id = 0x781;
d.product = "Sansa Clip+"_L1;
d.product_id = 0x74d0;
MTPDevice mtp_device;
mtp_device.vendor = "SanDisk"_L1;
mtp_device.vendor_id = 0x781;
mtp_device.product = "Sansa Clip+"_L1;
mtp_device.product_id = 0x74d0;
d.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
sMTPDeviceList << d;
mtp_device.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
sMTPDeviceList << mtp_device;
}
#endif // HAVE_MTP
run_loop_ = CFRunLoopGetCurrent();
@@ -240,12 +251,14 @@ CFTypeRef GetUSBRegistryEntry(io_object_t device, CFStringRef key) {
}
QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
ScopedCFTypeRef<CFStringRef> registry_string(reinterpret_cast<CFStringRef>(GetUSBRegistryEntry(device, key)));
if (registry_string) {
return QString::fromUtf8([reinterpret_cast<NSString*>(registry_string.get()) UTF8String]);
}
return QString();
}
NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
@@ -277,17 +290,13 @@ NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
int GetUSBDeviceClass(io_object_t device) {
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(
device,
kIOServicePlane,
CFSTR(kUSBInterfaceClass),
kCFAllocatorDefault,
kIORegistryIterateRecursively));
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(device, kIOServicePlane, CFSTR(kUSBInterfaceClass), kCFAllocatorDefault, kIORegistryIterateRecursively));
NSNumber *number = reinterpret_cast<NSNumber*>(interface_class.get());
if (number) {
int ret = [number unsignedShortValue];
return ret;
}
return 0;
}
@@ -322,12 +331,14 @@ QString GetSerialForDevice(io_object_t device) {
}
#ifdef HAVE_MTP
QString GetSerialForMTPDevice(io_object_t device) {
scoped_nsobject<NSString> serial(reinterpret_cast<NSString*>(GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString))));
return "MTP/"_L1 + QString::fromUtf8([serial UTF8String]);
}
#endif // HAVE_MTP
QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
@@ -343,6 +354,7 @@ QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
} // namespace
#ifdef HAVE_MTP
quint64 MacOsDeviceLister::GetFreeSpace(const QUrl &url) {
QMutexLocker l(&libmtp_mutex_);
@@ -380,6 +392,8 @@ quint64 MacOsDeviceLister::GetCapacity(const QUrl &url) {
}
#endif // HAVE_MTP
void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
@@ -390,12 +404,12 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
NSString *kind = [properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionMediaKindKey)];
if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) {
// CD inserted.
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
me->cd_devices_ << bsd_name;
Q_EMIT me->DeviceAdded(bsd_name);
return;
}
#endif
#endif // HAVE_AUDIOCD
NSURL *volume_path = [[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy];
@@ -403,8 +417,8 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
ScopedIOObject device(DADiskCopyIOMedia(disk));
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get()));
if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
CFMutableDictionaryRef cf_properties;
kern_return_t ret = IORegistryEntryCreateCFProperties(device.get(), &cf_properties, kCFAllocatorDefault, 0);
@@ -412,7 +426,7 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
if (ret == KERN_SUCCESS) {
scoped_nsobject<NSDictionary> dict(reinterpret_cast<NSDictionary*>(cf_properties)); // Takes ownership.
if ([[dict objectForKey:@"Removable"] intValue] == 1) {
QString serial = GetSerialForDevice(device.get());
const QString serial = GetSerialForDevice(device.get());
if (!serial.isEmpty()) {
me->current_devices_[serial] = QString::fromLatin1(DADiskGetBSDName(disk));
Q_EMIT me->DeviceAdded(serial);
@@ -427,10 +441,9 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
void MacOsDeviceLister::DiskRemovedCallback(DADiskRef disk, void *context) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
// We cannot access the USB tree when the disk is removed but we still get
// the BSD disk name.
// We cannot access the USB tree when the disk is removed but we still get the BSD disk name.
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
if (me->cd_devices_.remove(bsd_name)) {
Q_EMIT me->DeviceRemoved(bsd_name);
return;
@@ -496,6 +509,7 @@ int GetBusNumber(io_object_t o) {
void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
Q_UNUSED(me)
io_object_t object;
while ((object = IOIteratorNext(it))) {
@@ -503,30 +517,34 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
const int interface_class = GetUSBDeviceClass(object);
qLog(Debug) << "Interface class:" << interface_class;
#ifdef HAVE_MTP
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
int interface_class = GetUSBDeviceClass(object);
qLog(Debug) << "Interface class:" << interface_class;
QString serial = GetSerialForMTPDevice(object);
const QString serial = GetSerialForMTPDevice(object);
MTPDevice device;
device.vendor = QString::fromUtf8([vendor UTF8String]);
device.product = QString::fromUtf8([product UTF8String]);
device.vendor_id = [vendor_id unsignedShortValue];
device.product_id = [product_id unsignedShortValue];
device.quirks = 0;
MTPDevice mtp_device;
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
mtp_device.product = QString::fromUtf8([product UTF8String]);
mtp_device.vendor_id = [vendor_id unsignedShortValue];
mtp_device.product_id = [product_id unsignedShortValue];
mtp_device.quirks = 0;
device.bus = -1;
device.address = -1;
mtp_device.bus = -1;
mtp_device.address = -1;
if (device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
if (mtp_device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
// Blacklist ilok2 as this probe may be breaking it.
(device.vendor_id == 0x088e && device.product_id == 0x5036) ||
(mtp_device.vendor_id == 0x088e && mtp_device.product_id == 0x5036) ||
// Blacklist eLicenser
(device.vendor_id == 0x0819 && device.product_id == 0x0101) ||
(mtp_device.vendor_id == 0x0819 && mtp_device.product_id == 0x0101) ||
// Skip HID devices, printers and hubs.
interface_class == kUSBHIDInterfaceClass ||
interface_class == kUSBPrintingInterfaceClass ||
@@ -535,31 +553,28 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
}
NSNumber *addr = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR("USB Address")));
int bus = GetBusNumber(object);
const int bus = GetBusNumber(object);
if (!addr || bus == -1) {
// Failed to get bus or address number.
continue;
}
device.bus = bus;
device.address = [addr intValue];
mtp_device.bus = bus;
mtp_device.address = [addr intValue];
// First check the libmtp device list.
QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(device);
QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(mtp_device);
if (it2 != sMTPDeviceList.end()) {
// Fill in quirks flags from libmtp.
device.quirks = it2->quirks;
me->FoundMTPDevice(device, GetSerialForMTPDevice(object));
mtp_device.quirks = it2->quirks;
me->FoundMTPDevice(mtp_device, GetSerialForMTPDevice(object));
continue;
}
#endif // HAVE_MTP
IOCFPlugInInterface **plugin_interface = nullptr;
SInt32 score;
kern_return_t err = IOCreatePlugInInterfaceForService(
object,
kIOUSBDeviceUserClientTypeID,
kIOCFPlugInInterfaceID,
&plugin_interface,
&score);
kern_return_t err = IOCreatePlugInInterfaceForService(object, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin_interface, &score);
if (err != KERN_SUCCESS) {
continue;
}
@@ -590,7 +605,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
bool ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, 2, &data);
if (!ret) continue;
UInt8 string_len = data[0];
const UInt8 string_len = data[0];
ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, string_len, &data);
if (!ret) continue;
@@ -599,6 +614,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
// Because this was designed by MS, the characters are in UTF-16 (LE?).
QString str = QString::fromUtf16(reinterpret_cast<char16_t*>(data.data() + 2), (data.size() / 2) - 2);
#ifdef HAVE_MTP
if (str.startsWith("MSFT100"_L1)) {
// We got the OS descriptor!
char vendor_code = data[16];
@@ -621,8 +637,10 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
continue;
}
// Hurray! We made it!
me->FoundMTPDevice(device, serial);
me->FoundMTPDevice(mtp_device, serial);
}
#endif // HAVE_MTP
}
}
@@ -631,30 +649,39 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
void MacOsDeviceLister::USBDeviceRemovedCallback(void *refcon, io_iterator_t it) {
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
Q_UNUSED(me)
io_object_t object;
while ((object = IOIteratorNext(it))) {
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
#ifdef HAVE_MTP
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
QString serial = GetSerialForMTPDevice(object);
MTPDevice device;
device.vendor = QString::fromUtf8([vendor UTF8String]);
device.product = QString::fromUtf8([product UTF8String]);
device.vendor_id = [vendor_id unsignedShortValue];
device.product_id = [product_id unsignedShortValue];
const QString serial = GetSerialForMTPDevice(object);
MTPDevice mtp_device;
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
mtp_device.product = QString::fromUtf8([product UTF8String]);
mtp_device.vendor_id = [vendor_id unsignedShortValue];
mtp_device.product_id = [product_id unsignedShortValue];
me->RemovedMTPDevice(serial);
#endif // HAVE_MTP
}
}
}
#ifdef HAVE_MTP
void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
int count = mtp_devices_.remove(serial);
@@ -668,34 +695,40 @@ void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
void MacOsDeviceLister::FoundMTPDevice(const MTPDevice &device, const QString &serial) {
qLog(Debug) << "New MTP device detected!" << device.bus << device.address;
mtp_devices_[serial] = device;
QList<QUrl> urls = MakeDeviceUrls(serial);
MTPDevice *d = &mtp_devices_[serial];
d->capacity = GetCapacity(urls[0]);
d->free_space = GetFreeSpace(urls[0]);
const QList<QUrl> urls = MakeDeviceUrls(serial);
MTPDevice *mtp_device = &mtp_devices_[serial];
mtp_device->capacity = GetCapacity(urls[0]);
mtp_device->free_space = GetFreeSpace(urls[0]);
Q_EMIT DeviceAdded(serial);
}
bool IsMTPSerial(const QString &serial) { return serial.startsWith("MTP"_L1); }
#endif // HAVE_MTP
bool MacOsDeviceLister::IsCDDevice(const QString &serial) const {
return cd_devices_.contains(serial);
}
QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
const MTPDevice &device = mtp_devices_[serial];
if (device.vendor.isEmpty()) {
return device.product;
const MTPDevice &mtp_device = mtp_devices_[serial];
if (mtp_device.vendor.isEmpty()) {
return mtp_device.product;
}
else {
return device.vendor + QLatin1Char(' ') + device.product;
return mtp_device.vendor + QLatin1Char(' ') + mtp_device.product;
}
}
#endif // HAVE_MTP
QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
const QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
@@ -708,75 +741,86 @@ QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
ScopedIOObject device(DADiskCopyIOMedia(disk));
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
if (vendor.isEmpty()) {
return product;
}
return vendor + QLatin1Char(' ') + product;
}
QList<QUrl> MacOsDeviceLister::MakeDeviceUrls(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
const MTPDevice &device = mtp_devices_[serial];
QString str = QString::asprintf("gphoto2://usb-%d-%d/", device.bus, device.address);
const MTPDevice &mtp_device = mtp_devices_[serial];
const QString str = QString::asprintf("gphoto2://usb-%d-%d/", mtp_device.bus, mtp_device.address);
QUrlQuery url_query;
url_query.addQueryItem(u"vendor"_s, device.vendor);
url_query.addQueryItem(u"vendor_id"_s, QString::number(device.vendor_id));
url_query.addQueryItem(u"product"_s, device.product);
url_query.addQueryItem(u"product_id"_s, QString::number(device.product_id));
url_query.addQueryItem(u"quirks"_s, QString::number(device.quirks));
url_query.addQueryItem(u"vendor"_s, mtp_device.vendor);
url_query.addQueryItem(u"vendor_id"_s, QString::number(mtp_device.vendor_id));
url_query.addQueryItem(u"product"_s, mtp_device.product);
url_query.addQueryItem(u"product_id"_s, QString::number(mtp_device.product_id));
url_query.addQueryItem(u"quirks"_s, QString::number(mtp_device.quirks));
QUrl url(str);
url.setQuery(url_query);
return QList<QUrl>() << url;
}
#endif // HAVE_MTP
if (IsCDDevice(serial)) {
return QList<QUrl>() << QUrl(u"cdda:///dev/r"_s + serial);
}
QString bsd_name = current_devices_[serial];
const QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk.get())));
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
QUrl ret = MakeUrlFromLocalPath(path);
const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
const QUrl ret = MakeUrlFromLocalPath(path);
return QList<QUrl>() << ret;
}
QStringList MacOsDeviceLister::DeviceUniqueIDs() {
#ifdef HAVE_MTP
return current_devices_.keys() + mtp_devices_.keys();
#else
return current_devices_.keys();
#endif
}
QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
return QVariantList();
}
#endif // HAVE_MTP
if (IsCDDevice(serial)) {
return QVariantList() << u"media-optical"_s;
}
QString bsd_name = current_devices_[serial];
const QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
ScopedIOObject device(DADiskCopyIOMedia(disk.get()));
QString icon = GetIconForDevice(device.get());
const QString icon = GetIconForDevice(device.get());
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk)));
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
QVariantList ret;
ret << GuessIconForPath(path);
@@ -784,31 +828,45 @@ QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
if (!icon.isEmpty()) {
ret << icon;
}
return ret;
}
QString MacOsDeviceLister::DeviceManufacturer(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
return mtp_devices_[serial].vendor;
}
#endif // HAVE_MTP
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
}
QString MacOsDeviceLister::DeviceModel(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
return mtp_devices_[serial].product;
}
#endif // HAVE_MTP
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
}
quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
QList<QUrl> urls = MakeDeviceUrls(serial);
return mtp_devices_[serial].capacity;
}
QString bsd_name = current_devices_[serial];
#endif // HAVE_MTP
const QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
@@ -816,7 +874,7 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
NSNumber *capacity = reinterpret_cast<NSNumber*>(GetPropertyForDevice(device, CFSTR("Size")));
quint64 ret = [capacity unsignedLongLongValue];
const quint64 ret = [capacity unsignedLongLongValue];
IOObjectRelease(device);
@@ -826,10 +884,13 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
quint64 MacOsDeviceLister::DeviceFreeSpace(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
QList<QUrl> urls = MakeDeviceUrls(serial);
return mtp_devices_[serial].free_space;
}
#endif // HAVE_MTP
QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
@@ -857,9 +918,11 @@ bool MacOsDeviceLister::AskForScan(const QString &serial) const {
void MacOsDeviceLister::UnmountDevice(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) return;
#endif
QString bsd_name = current_devices_[serial];
const QString bsd_name = current_devices_[serial];
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, loop_session_, bsd_name.toLatin1().constData()));
DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);
@@ -879,13 +942,16 @@ void MacOsDeviceLister::DiskUnmountCallback(DADiskRef disk, DADissenterRef disse
void MacOsDeviceLister::UpdateDeviceFreeSpace(const QString &serial) {
#ifdef HAVE_MTP
if (IsMTPSerial(serial)) {
if (mtp_devices_.contains(serial)) {
QList<QUrl> urls = MakeDeviceUrls(serial);
MTPDevice *d = &mtp_devices_[serial];
d->free_space = GetFreeSpace(urls[0]);
MTPDevice *mtp_device = &mtp_devices_[serial];
mtp_device->free_space = GetFreeSpace(urls[0]);
}
}
#endif
Q_EMIT DeviceChanged(serial);
}

View File

@@ -46,7 +46,8 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
: QObject(parent),
player_(player),
playlist_manager_(playlist_manager),
initialized_(false) {
initialized_(false),
status_display_type_(0) {
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged);
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged);
@@ -69,6 +70,7 @@ void RichPresence::ReloadSettings() {
Settings s;
s.beginGroup(DiscordRPCSettings::kSettingsGroup);
const bool enabled = s.value(DiscordRPCSettings::kEnabled, false).toBool();
status_display_type_ = s.value(DiscordRPCSettings::kStatusDisplayType, static_cast<int>(DiscordRPCSettings::StatusDisplayType::App)).toInt();
s.endGroup();
if (enabled && !initialized_) {
@@ -117,7 +119,11 @@ void RichPresence::SendPresenceUpdate() {
::DiscordRichPresence presence_data{};
memset(&presence_data, 0, sizeof(presence_data));
presence_data.type = 2; // Listening
// Listening to
presence_data.type = 2;
presence_data.status_display_type = status_display_type_;
presence_data.largeImageKey = kStrawberryIconResourceName;
presence_data.smallImageKey = kStrawberryIconResourceName;
presence_data.smallImageText = kStrawberryIconDescription;
@@ -126,7 +132,9 @@ void RichPresence::SendPresenceUpdate() {
QByteArray artist;
if (!activity_.artist.isEmpty()) {
artist = activity_.artist.toUtf8();
artist.prepend(tr("by ").toUtf8());
if (artist.size() < 2) { // Discord activity 2 char min. fix
artist.append(" ");
}
presence_data.state = artist.constData();
}

View File

@@ -70,6 +70,7 @@ class RichPresence : public QObject {
};
Activity activity_;
bool initialized_;
int status_display_type_;
};
} // namespace discord

View File

@@ -102,7 +102,7 @@ class MusicBrainzClient : public QObject {
void CancelAll();
Q_SIGNALS:
// Finished signal emitted when fechting songs tags
// Finished signal emitted when feching songs tags
void Finished(const int id, const MusicBrainzClient::ResultList &result, const QString &error = QString());
// Finished signal emitted when fechting album's songs tags using DiscId
void DiscIdFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &result, const QString &error = QString());

View File

@@ -96,6 +96,7 @@ void SongLoaderInserter::Load(Playlist *destination, const int row, const bool p
else {
(void)QtConcurrent::run(&SongLoaderInserter::AsyncLoad, this);
}
}
// Load audio CD tracks:
@@ -111,13 +112,15 @@ void SongLoaderInserter::LoadAudioCD(Playlist *destination, const int row, const
enqueue_next_ = enqueue_next;
SongLoader *loader = new SongLoader(url_handlers_, collection_backend_, tagreader_client_, this);
QObject::connect(loader, &SongLoader::AudioCDTracksLoadFinished, this, [this, loader]() { AudioCDTracksLoadFinished(loader); });
QObject::connect(loader, &SongLoader::LoadAudioCDFinished, this, &SongLoaderInserter::AudioCDTagsLoaded);
QObject::connect(loader, &SongLoader::AudioCDTracksLoaded, this, &SongLoaderInserter::AudioCDTracksLoadedSlot);
QObject::connect(loader, &SongLoader::AudioCDTracksUpdated, this, &SongLoaderInserter::AudioCDTracksUpdatedSlot);
QObject::connect(loader, &SongLoader::AudioCDLoadingFinished, this, &SongLoaderInserter::AudioCDLoadingFinishedSlot);
qLog(Info) << "Loading audio CD...";
const SongLoader::Result result = loader->LoadAudioCD();
if (result == SongLoader::Result::Error) {
if (loader->errors().isEmpty())
if (loader->errors().isEmpty()) {
Q_EMIT Error(tr("Error while loading audio CD."));
}
else {
const QStringList errors = loader->errors();
for (const QString &error : errors) {
@@ -126,13 +129,17 @@ void SongLoaderInserter::LoadAudioCD(Playlist *destination, const int row, const
}
delete loader;
}
// Songs will be loaded later: see AudioCDTracksLoadFinished and AudioCDTagsLoaded slots
}
void SongLoaderInserter::DestinationDestroyed() { destination_ = nullptr; }
void SongLoaderInserter::AudioCDTracksLoadFinished(SongLoader *loader) {
void SongLoaderInserter::AudioCDTracksLoadedSlot() {
SongLoader *loader = qobject_cast<SongLoader*>(sender());
if (!loader) return;
songs_ = loader->songs();
if (songs_.isEmpty()) {
@@ -147,17 +154,18 @@ void SongLoaderInserter::AudioCDTracksLoadFinished(SongLoader *loader) {
}
void SongLoaderInserter::AudioCDTagsLoaded(const bool success) {
void SongLoaderInserter::AudioCDTracksUpdatedSlot() {
SongLoader *loader = qobject_cast<SongLoader*>(sender());
if (!loader || !destination_) return;
if (!loader || loader->songs().isEmpty() || !destination_) return;
if (success) {
destination_->UpdateItems(loader->songs());
}
else {
qLog(Error) << "Error while getting audio CD metadata from MusicBrainz";
}
destination_->UpdateItems(loader->songs());
}
void SongLoaderInserter::AudioCDLoadingFinishedSlot(const bool success) {
Q_UNUSED(success)
deleteLater();

View File

@@ -62,8 +62,9 @@ class SongLoaderInserter : public QObject {
private Q_SLOTS:
void DestinationDestroyed();
void AudioCDTracksLoadFinished(SongLoader *loader);
void AudioCDTagsLoaded(const bool success);
void AudioCDTracksLoadedSlot();
void AudioCDTracksUpdatedSlot();
void AudioCDLoadingFinishedSlot(const bool success);
void InsertSongs();
private:

View File

@@ -209,6 +209,19 @@ void NotificationsSettingsPage::Load() {
// Discord
s.beginGroup(DiscordRPCSettings::kSettingsGroup);
ui_->richpresence_enabled->setChecked(s.value(DiscordRPCSettings::kEnabled, false).toBool());
const DiscordRPCSettings::StatusDisplayType discord_status_display_type = static_cast<DiscordRPCSettings::StatusDisplayType>(s.value(DiscordRPCSettings::kStatusDisplayType, static_cast<int>(DiscordRPCSettings::StatusDisplayType::App)).toInt());
switch (discord_status_display_type) {
case DiscordRPCSettings::StatusDisplayType::App:
ui_->richpresence_listening_to_app->setChecked(true);
break;
case DiscordRPCSettings::StatusDisplayType::Artist:
ui_->richpresence_listening_to_artist->setChecked(true);
break;
case DiscordRPCSettings::StatusDisplayType::Song:
ui_->richpresence_listening_to_song->setChecked(true);
break;
}
s.endGroup();
UpdatePopupVisible();
@@ -229,6 +242,11 @@ void NotificationsSettingsPage::Save() {
else if (osd_->SupportsTrayPopups() && ui_->notifications_tray->isChecked()) osd_type = OSDSettings::Type::TrayPopup;
else if (osd_->SupportsOSDPretty() && ui_->notifications_pretty->isChecked()) osd_type = OSDSettings::Type::Pretty;
DiscordRPCSettings::StatusDisplayType discord_status_display_type = DiscordRPCSettings::StatusDisplayType::App;
if (ui_->richpresence_listening_to_app->isChecked()) discord_status_display_type = DiscordRPCSettings::StatusDisplayType::App;
else if (ui_->richpresence_listening_to_artist->isChecked()) discord_status_display_type = DiscordRPCSettings::StatusDisplayType::Artist;
else if (ui_->richpresence_listening_to_song->isChecked()) discord_status_display_type = DiscordRPCSettings::StatusDisplayType::Song;
s.beginGroup(OSDSettings::kSettingsGroup);
s.setValue(OSDSettings::kType, static_cast<int>(osd_type));
s.setValue(OSDSettings::kTimeout, ui_->notifications_duration->value() * 1000);
@@ -255,7 +273,9 @@ void NotificationsSettingsPage::Save() {
s.beginGroup(DiscordRPCSettings::kSettingsGroup);
s.setValue(DiscordRPCSettings::kEnabled, ui_->richpresence_enabled->isChecked());
s.setValue(DiscordRPCSettings::kStatusDisplayType, static_cast<int>(discord_status_display_type));
s.endGroup();
}
void NotificationsSettingsPage::PrettyOpacityChanged(int value) {

View File

@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>518</width>
<height>778</height>
<height>844</height>
</rect>
</property>
<property name="windowTitle">
@@ -380,6 +380,36 @@
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="richpresence_listening_to">
<property name="title">
<string>&quot;Listening to...&quot;</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_11">
<item>
<widget class="QRadioButton" name="richpresence_listening_to_app">
<property name="text">
<string>Strawberry</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="richpresence_listening_to_artist">
<property name="text">
<string>Artist name</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="richpresence_listening_to_song">
<property name="text">
<string>Song title</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -2,7 +2,7 @@
*Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -47,6 +47,7 @@ class SystemTrayIcon : public QObject {
bool isVisible() const { return true; }
void setVisible(const bool) {}
void SetDevicePixelRatioF(const qreal device_pixel_ratio);
void SetTrayiconProgress(const bool enabled);
void SetupMenu(QAction *previous, QAction *play, QAction *stop, QAction *stop_after, QAction *next, QAction *mute, QAction *love, QAction *quit);
@@ -93,6 +94,7 @@ class SystemTrayIcon : public QObject {
QPixmap playing_icon_;
QPixmap paused_icon_;
QPixmap current_state_icon_;
qreal device_pixel_ratio_;
bool trayicon_progress_;
int song_progress_;
Q_DISABLE_COPY(SystemTrayIcon);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -159,6 +159,7 @@ SystemTrayIcon::SystemTrayIcon(QObject *parent)
grey_icon_(QPixmap(u":/pictures/strawberry-grey.png"_s).scaled(128, 128, Qt::KeepAspectRatio, Qt::SmoothTransformation)),
playing_icon_(u":/pictures/tiny-play.png"_s),
paused_icon_(u":/pictures/tiny-pause.png"_s),
device_pixel_ratio_(1.0),
trayicon_progress_(false),
song_progress_(0) {
@@ -168,6 +169,12 @@ SystemTrayIcon::SystemTrayIcon(QObject *parent)
SystemTrayIcon::~SystemTrayIcon() {}
void SystemTrayIcon::SetDevicePixelRatioF(const qreal device_pixel_ratio) {
device_pixel_ratio_ = device_pixel_ratio;
}
void SystemTrayIcon::SetTrayiconProgress(const bool enabled) {
trayicon_progress_ = enabled;

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,33 +36,30 @@
using namespace Qt::Literals::StringLiterals;
namespace {
constexpr int kSystemTrayIconSize = 48;
}
SystemTrayIcon::SystemTrayIcon(QObject *parent)
: QSystemTrayIcon(parent),
menu_(new QMenu),
pixmap_playing_(u":/pictures/tiny-play.png"_s),
pixmap_paused_(u":/pictures/tiny-pause.png"_s),
icon_normal_(IconLoader::Load(u"strawberry"_s)),
icon_grey_(IconLoader::Load(u"strawberry-grey"_s)),
icon_playing_(QIcon(u":/pictures/tiny-play.png"_s)),
icon_paused_(QIcon(u":/pictures/tiny-pause.png"_s)),
action_play_pause_(nullptr),
action_stop_(nullptr),
action_stop_after_this_track_(nullptr),
action_mute_(nullptr),
action_love_(nullptr),
available_(false),
device_pixel_ratio_(1.0),
trayicon_progress_(false),
song_progress_(0) {
const QIcon icon = IconLoader::Load(u"strawberry"_s);
const QIcon icon_grey = IconLoader::Load(u"strawberry-grey"_s);
pixmap_normal_ = icon.pixmap(48, QIcon::Normal);
if (icon_grey.isNull()) {
pixmap_grey_ = icon.pixmap(48, QIcon::Disabled);
}
else {
pixmap_grey_ = icon_grey.pixmap(48, QIcon::Disabled);
}
if (isSystemTrayAvailable()) {
available_ = true;
setIcon(icon);
setIcon(icon_normal_);
setToolTip(QCoreApplication::applicationName());
}
@@ -74,6 +71,41 @@ SystemTrayIcon::~SystemTrayIcon() {
delete menu_;
}
void SystemTrayIcon::InitPixmaps() {
if (pixmap_normal_.isNull() || pixmap_normal_.devicePixelRatioF() != device_pixel_ratio_) {
pixmap_normal_ = icon_normal_.pixmap(QSize(kSystemTrayIconSize, kSystemTrayIconSize), device_pixel_ratio_, QIcon::Normal);
}
if (pixmap_grey_.isNull() || pixmap_grey_.devicePixelRatioF() != device_pixel_ratio_) {
if (icon_grey_.isNull()) {
pixmap_grey_ = icon_normal_.pixmap(QSize(kSystemTrayIconSize, kSystemTrayIconSize), device_pixel_ratio_, QIcon::Disabled);
}
else {
pixmap_grey_ = icon_grey_.pixmap(QSize(kSystemTrayIconSize, kSystemTrayIconSize), device_pixel_ratio_, QIcon::Disabled);
}
}
if (pixmap_playing_.isNull() || pixmap_playing_.devicePixelRatioF() != device_pixel_ratio_) {
pixmap_playing_ = icon_playing_.pixmap(icon_playing_.availableSizes().at(0));
pixmap_playing_.setDevicePixelRatio(device_pixel_ratio_);
}
if (pixmap_paused_.isNull() || pixmap_paused_.devicePixelRatioF() != device_pixel_ratio_) {
pixmap_paused_ = icon_paused_.pixmap(icon_paused_.availableSizes().at(0));
pixmap_paused_.setDevicePixelRatio(device_pixel_ratio_);
}
}
void SystemTrayIcon::SetDevicePixelRatioF(const qreal device_pixel_ratio) {
device_pixel_ratio_ = device_pixel_ratio;
InitPixmaps();
}
void SystemTrayIcon::SetTrayiconProgress(const bool enabled) {
trayicon_progress_ = enabled;
@@ -131,13 +163,17 @@ void SystemTrayIcon::ShowPopup(const QString &summary, const QString &message, c
void SystemTrayIcon::UpdateIcon() {
if (available_) setIcon(CreateIcon(pixmap_normal_, pixmap_grey_));
if (available_) {
InitPixmaps();
setIcon(CreateIcon(pixmap_normal_, pixmap_grey_));
}
}
void SystemTrayIcon::SetPlaying(bool enable_play_pause) {
void SystemTrayIcon::SetPlaying(const bool enable_play_pause) {
current_state_icon_ = pixmap_playing_;
UpdateIcon();
action_stop_->setEnabled(true);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -46,6 +46,9 @@ class SystemTrayIcon : public QSystemTrayIcon {
bool IsSystemTrayAvailable() const { return available_; }
void InitPixmaps();
void SetDevicePixelRatioF(const qreal device_pixel_ratio);
void SetTrayiconProgress(const bool enabled);
void SetupMenu(QAction *previous, QAction *play, QAction *stop, QAction *stop_after, QAction *next, QAction *mute, QAction *love, QAction *quit);
@@ -82,6 +85,12 @@ class SystemTrayIcon : public QSystemTrayIcon {
private:
QMenu *menu_;
QIcon icon_normal_;
QIcon icon_grey_;
QIcon icon_playing_;
QIcon icon_paused_;
QPixmap pixmap_normal_;
QPixmap pixmap_grey_;
QPixmap pixmap_playing_;
@@ -94,6 +103,7 @@ class SystemTrayIcon : public QSystemTrayIcon {
QAction *action_love_;
bool available_;
qreal device_pixel_ratio_;
bool trayicon_progress_;
int song_progress_;

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,45 +35,54 @@
# include "qtsystemtrayicon.h"
#endif
QPixmap SystemTrayIcon::CreateIcon(const QPixmap &icon, const QPixmap &grey_icon) {
QPixmap SystemTrayIcon::CreateIcon(const QPixmap &pixmap_icon_normal, const QPixmap &pixmap_icon_grey) {
QRect rect(icon.rect());
const qreal dpr = pixmap_icon_normal.devicePixelRatio();
const QRect pixmap_rect = pixmap_icon_normal.rect();
QPixmap ret(icon);
QPainter p(&ret);
QPixmap pixmap_drawn(pixmap_icon_normal.size());
pixmap_drawn.setDevicePixelRatio(dpr);
pixmap_drawn.fill(Qt::transparent);
QPainter p(&pixmap_drawn);
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
p.drawPixmap(0, 0, pixmap_icon_normal);
if (trayicon_progress_) {
// The angle of the line that's used to cover the icon.
// Centered on rect.topLeft()
double angle = static_cast<double>(100 - song_progress_) / 100.0 * M_PI_2;
double length = sqrt(pow(rect.width(), 2.0) + pow(rect.height(), 2.0));
const double angle = static_cast<double>(100 - song_progress_) / 100.0 * M_PI_2;
const double length = std::sqrt(std::pow(pixmap_rect.width(), 2.0) + std::pow(pixmap_rect.height(), 2.0));
QPolygon mask;
mask << rect.topLeft();
mask << rect.topLeft() + QPoint(static_cast<int>(length * sin(angle)), static_cast<int>(length * cos(angle)));
mask << pixmap_rect.topLeft();
mask << pixmap_rect.topLeft() + QPoint(static_cast<int>(length * std::sin(angle)), static_cast<int>(length * std::cos(angle)));
if (song_progress_ > 50) mask << rect.bottomRight();
if (song_progress_ > 50) {
mask << pixmap_rect.bottomRight();
}
mask << rect.topRight();
mask << rect.topLeft();
mask << pixmap_rect.topRight();
mask << pixmap_rect.topLeft();
// Draw the grey bit
p.setClipRegion(mask);
p.drawPixmap(0, 0, grey_icon);
p.drawPixmap(0, 0, pixmap_icon_grey);
p.setClipping(false);
}
// Draw the playing or paused icon in the top-right
if (!current_state_icon_.isNull()) {
int height = rect.height() / 2;
QPixmap scaled(current_state_icon_.scaledToHeight(height, Qt::SmoothTransformation));
const int height = pixmap_rect.height() / 2;
QPixmap current_state_scaled = current_state_icon_.scaledToHeight(height, Qt::SmoothTransformation);
current_state_scaled.setDevicePixelRatio(dpr);
QRect state_rect(rect.width() - scaled.width(), 0, scaled.width(), scaled.height());
p.drawPixmap(state_rect, scaled);
const QRect state_rect(static_cast<int>((pixmap_rect.width() - current_state_scaled.width()) / dpr), 0, static_cast<int>(current_state_scaled.width() / dpr), static_cast<int>(current_state_scaled.height() / dpr));
p.drawPixmap(state_rect, current_state_scaled);
}
p.end();
return ret;
return pixmap_drawn;
}

View File

@@ -371,7 +371,7 @@ TagReaderResult TagReaderTagLib::Read(SharedPtr<TagLib::FileRef> fileref, Song *
TagLib::List<TagLib::FLAC::Picture*> pictures = vorbis_comment->pictureList();
if (!pictures.isEmpty()) {
for (TagLib::FLAC::Picture *picture : pictures) {
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
if ((picture->type() == TagLib::FLAC::Picture::FrontCover || picture->type() == TagLib::FLAC::Picture::Other) && picture->data().size() > 0) {
song->set_art_embedded(true);
break;
}
@@ -388,7 +388,7 @@ TagReaderResult TagReaderTagLib::Read(SharedPtr<TagLib::FileRef> fileref, Song *
TagLib::List<TagLib::FLAC::Picture*> pictures = file_flac->pictureList();
if (!pictures.isEmpty()) {
for (TagLib::FLAC::Picture *picture : pictures) {
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
if ((picture->type() == TagLib::FLAC::Picture::FrontCover || picture->type() == TagLib::FLAC::Picture::Other) && picture->data().size() > 0) {
song->set_art_embedded(true);
break;
}
@@ -1472,12 +1472,19 @@ TagReaderResult TagReaderTagLib::LoadEmbeddedCover(const QString &filename, QByt
TagLib::List<TagLib::FLAC::Picture*> pictures = file_flac->pictureList();
if (!pictures.isEmpty()) {
for (TagLib::FLAC::Picture *picture : pictures) {
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
data = QByteArray(picture->data().data(), picture->data().size());
if (!data.isEmpty()) {
return TagReaderResult::ErrorCode::Success;
}
if (picture->data().size() <= 0) {
continue;
}
if (picture->type() == TagLib::FLAC::Picture::FrontCover) {
data = QByteArray(picture->data().data(), picture->data().size());
return TagReaderResult::ErrorCode::Success;
}
else if (picture->type() == TagLib::FLAC::Picture::Other) {
data = QByteArray(picture->data().data(), picture->data().size());
}
}
if (!data.isEmpty()) {
return TagReaderResult::ErrorCode::Success;
}
}
}
@@ -1515,21 +1522,27 @@ TagReaderResult TagReaderTagLib::LoadEmbeddedCover(const QString &filename, QByt
// Ogg Vorbis / Opus / Speex
if (TagLib::Ogg::XiphComment *vorbis_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
TagLib::Ogg::FieldListMap map = vorbis_comment->fieldListMap();
TagLib::List<TagLib::FLAC::Picture*> pictures = vorbis_comment->pictureList();
if (!pictures.isEmpty()) {
for (TagLib::FLAC::Picture *picture : pictures) {
if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) {
data = QByteArray(picture->data().data(), picture->data().size());
if (!data.isEmpty()) {
return TagReaderResult::ErrorCode::Success;
}
if (picture->data().size() <= 0) {
continue;
}
if (picture->type() == TagLib::FLAC::Picture::FrontCover) {
data = QByteArray(picture->data().data(), picture->data().size());
return TagReaderResult::ErrorCode::Success;
}
else if (picture->type() == TagLib::FLAC::Picture::Other) {
data = QByteArray(picture->data().data(), picture->data().size());
}
}
if (!data.isEmpty()) {
return TagReaderResult::ErrorCode::Success;
}
}
// Ogg lacks a definitive standard for embedding cover art, but it seems b64 encoding a field called COVERART is the general convention
const TagLib::Ogg::FieldListMap map = vorbis_comment->fieldListMap();
if (map.contains(kVorbisComment_CoverArt)) {
data = QByteArray::fromBase64(map[kVorbisComment_CoverArt].toString().toCString());
if (!data.isEmpty()) {