Compare commits

..

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
07900f1265 Extract retry logging into helper method and fix formatting
- Add LogRetryAttempt() helper method for consistent logging
- Fix formatting in ShouldRetryRequest() for better readability
- Use helper method in both GetRecentTracksRequestFinished and GetTopTracksRequestFinished
- Eliminates duplicate logging code

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 21:30:38 +00:00
copilot-swe-agent[bot]
c25d8a5e6c Improve retry logic safety and readability
- Add named constants for retry-eligible HTTP status codes (500, 503)
- Add bounds checking in backoff calculation to prevent integer overflow
- Use kMaxBackoffShift constant to limit bit shift operations
- Improves code safety and readability

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 21:29:11 +00:00
copilot-swe-agent[bot]
8bdbeeb5a8 Refactor retry logic to reduce code duplication
- Extract retry condition check into ShouldRetryRequest() helper
- Extract backoff delay calculation into CalculateBackoffDelay() helper
- Use helper methods in both GetRecentTracksRequestFinished and GetTopTracksRequestFinished
- Improves code maintainability and consistency

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 21:27:23 +00:00
copilot-swe-agent[bot]
4c8103ef6d Add custom API key support and retry logic for Last.fm import
- Add API key field to Last.fm settings UI with helpful info text
- Store and load custom API key from settings
- Use custom API key in lastfmimport if provided, fall back to default
- Implement exponential backoff retry logic (up to 5 retries)
- Retry on HTTP 500/503 errors with increasing delays (5s, 10s, 20s, 40s, 80s)
- Add retry count tracking to request structures

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 21:24:41 +00:00
copilot-swe-agent[bot]
8a7a22e9bd Initial plan 2026-01-03 21:12:16 +00:00
25 changed files with 309 additions and 473 deletions

View File

@@ -747,7 +747,7 @@ jobs:
df -h
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.3.7
uses: vmactions/freebsd-vm@v1.3.5
with:
usesh: true
mem: 8192

View File

@@ -84,6 +84,8 @@ if(MSVC)
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
else()
list(APPEND COMPILE_OPTIONS
$<$<COMPILE_LANGUAGE:C>:-std=c11>
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
-Wall
-Wextra
-Wpedantic
@@ -253,6 +255,7 @@ find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
if(APPLE)
find_library(SPARKLE Sparkle)
#find_package(SPMediaKeyTap REQUIRED)
endif()
if(WIN32)
@@ -1215,10 +1218,6 @@ set(UI
src/device/deviceviewcontainer.ui
)
if(UNIX)
optional_source(UNIX SOURCES src/core/unixsignalwatcher.cpp HEADERS src/core/unixsignalwatcher.h)
endif()
if(APPLE)
optional_source(APPLE
SOURCES

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, 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
@@ -537,24 +537,10 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
ScopedTransaction transaction(&db);
for (const CollectionSubdirectory &subdir : subdirs) {
// See if this subdirectory already exists in the database
bool exists = false;
{
if (subdir.mtime == 0) {
// Delete the subdirectory
SqlQuery q(db);
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
exists = q.next();
}
if (exists) {
SqlQuery q(db);
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":mtime"_s, subdir.mtime);
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
@@ -563,36 +549,42 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
}
}
else {
SqlQuery q(db);
q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
q.BindValue(u":mtime"_s, subdir.mtime);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
// See if this subdirectory already exists in the database
bool exists = false;
{
SqlQuery q(db);
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
exists = q.next();
}
}
}
transaction.Commit();
}
void CollectionBackend::DeleteSubdirs(const CollectionSubdirectoryList &subdirs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction transaction(&db);
for (const CollectionSubdirectory &subdir : subdirs) {
SqlQuery q(db);
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
if (exists) {
SqlQuery q(db);
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
q.BindValue(u":mtime"_s, subdir.mtime);
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
else {
SqlQuery q(db);
q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
q.BindValue(u":id"_s, subdir.directory_id);
q.BindValue(u":path"_s, subdir.path);
q.BindValue(u":mtime"_s, subdir.mtime);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
}
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, 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
@@ -252,7 +252,6 @@ class CollectionBackend : public CollectionBackendInterface {
void DeleteSongsByUrls(const QList<QUrl> &url);
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
void DeleteSubdirs(const CollectionSubdirectoryList &subdirs);
void CompilationsNeedUpdating();
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);

View File

@@ -124,7 +124,6 @@ void CollectionLibrary::Init() {
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(watcher_, &CollectionWatcher::SubdirsDeleted, &*backend_, &CollectionBackend::DeleteSubdirs);
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, 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
@@ -261,7 +261,7 @@ void CollectionWatcher::ReloadSettings() {
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, const int dir, const bool incremental, const bool ignores_mtime, const bool mark_songs_unavailable)
: progress_(0),
progress_max_(0),
dir_id_(dir),
dir_(dir),
incremental_(incremental),
ignores_mtime_(ignores_mtime),
mark_songs_unavailable_(mark_songs_unavailable),
@@ -313,19 +313,6 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
if (!deleted_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsDeleted(deleted_subdirs);
}
if (!new_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
}
if (!touched_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
touched_subdirs.clear();
}
if (!deleted_songs.isEmpty()) {
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
Q_EMIT watcher_->SongsUnavailable(deleted_songs);
@@ -351,24 +338,34 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
readded_songs.clear();
}
if (!new_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
}
if (!touched_subdirs.isEmpty()) {
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
touched_subdirs.clear();
}
for (const CollectionSubdirectory &subdir : std::as_const(deleted_subdirs)) {
if (watcher_->watched_dirs_.contains(dir_id_)) {
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_id_], subdir);
if (watcher_->watched_dirs_.contains(dir_)) {
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
}
}
deleted_subdirs.clear();
if (watcher_->monitor_) {
// Watch the new subdirectories
for (const CollectionSubdirectory &subdir : std::as_const(new_subdirs)) {
if (watcher_->watched_dirs_.contains(dir_id_)) {
watcher_->AddWatch(watcher_->watched_dirs_[dir_id_], subdir.path);
if (watcher_->watched_dirs_.contains(dir_)) {
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
}
}
}
new_subdirs.clear();
if (incremental_ || ignores_mtime_) {
Q_EMIT watcher_->UpdateLastSeen(dir_id_, expire_unavailable_songs_days_);
Q_EMIT watcher_->UpdateLastSeen(dir_, expire_unavailable_songs_days_);
}
}
@@ -377,7 +374,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
if (cached_songs_dirty_) {
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_id_);
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
cached_songs_.insert(p, song);
@@ -396,7 +393,7 @@ SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QStri
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QString &path) {
if (cached_songs_missing_fingerprint_dirty_) {
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_id_);
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
cached_songs_missing_fingerprint_.insert(p, song);
@@ -411,7 +408,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QS
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingLoudnessCharacteristics(const QString &path) {
if (cached_songs_missing_loudness_characteristics_dirty_) {
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_id_);
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_);
for (const Song &song : songs) {
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
cached_songs_missing_loudness_characteristics_.insert(p, song);
@@ -433,7 +430,7 @@ void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const CollectionSubdire
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
}
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
@@ -443,7 +440,7 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
}
CollectionSubdirectoryList ret;
@@ -460,7 +457,7 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdi
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
}
return known_subdirs_;
@@ -497,7 +494,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
const quint64 files_count = FilesCountForPath(&transaction, dir.path);
transaction.SetKnownSubdirs(subdirs);
transaction.AddToProgressMax(files_count);
ScanSubdirectory(dir, dir.path, CollectionSubdirectory(), files_count, &transaction);
ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction);
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
}
else {
@@ -515,7 +512,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
transaction.AddToProgressMax(files_count);
for (const CollectionSubdirectory &subdir : subdirs) {
if (stop_or_abort_requested()) break;
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
}
if (!stop_or_abort_requested()) {
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
@@ -527,7 +524,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
}
void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
const QFileInfo path_info(path);
@@ -539,8 +536,8 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
return;
}
// Do not scan symlinked dirs that are already in collection
for (const CollectionDirectory &i : std::as_const(watched_dirs_)) {
if (real_path.startsWith(i.path)) {
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
return;
}
}
@@ -581,7 +578,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
const CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
ScanSubdirectory(dir, prev_subdir.path, prev_subdir, 0, t, true);
ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true);
}
}
@@ -623,9 +620,12 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
album_art[dir_part] << child_filepath;
t->AddToProgress(1);
}
else {
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
files_on_disk << child_filepath;
}
else {
t->AddToProgress(1);
}
}
}
@@ -727,9 +727,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
#endif
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t)) {
files_on_disk.removeAll(file);
}
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t);
}
else { // If CUE associated.
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
@@ -786,9 +784,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
const QUrl art_automatic = ArtForSong(file, album_art);
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t)) {
files_on_disk.removeAll(file);
}
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t);
}
else { // If CUE associated.
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
@@ -799,7 +795,6 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
if (songs.isEmpty()) {
files_on_disk.removeAll(file);
t->AddToProgress(1);
continue;
}
@@ -810,7 +805,7 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
const QUrl art_automatic = ArtForSong(file, album_art);
for (Song song : songs) {
song.set_directory_id(t->dir_id());
song.set_directory_id(t->dir());
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
t->new_songs << song;
}
@@ -828,26 +823,27 @@ void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const Q
}
}
// Add, update or delete subdir
// Add this subdir to the new or touched list
CollectionSubdirectory updated_subdir;
updated_subdir.directory_id = t->dir_id();
updated_subdir.directory_id = t->dir();
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
updated_subdir.path = path;
if (updated_subdir.mtime == 0 && updated_subdir.path != dir.path) {
t->deleted_subdirs << updated_subdir;
}
else if (subdir.directory_id == -1) {
if (subdir.directory_id == -1) {
t->new_subdirs << updated_subdir;
}
else if (subdir.mtime != updated_subdir.mtime) {
else {
t->touched_subdirs << updated_subdir;
}
if (updated_subdir.mtime == 0) { // CollectionSubdirectory deleted, mark it for removal from the watcher.
t->deleted_subdirs << updated_subdir;
}
// Recurse into the new subdirs that we found
for (const CollectionSubdirectory &my_new_subdir : std::as_const(my_new_subdirs)) {
if (stop_or_abort_requested()) return;
ScanSubdirectory(dir, my_new_subdir.path, my_new_subdir, 0, t, true);
ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true);
}
}
@@ -879,7 +875,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
QSet<int> used_ids;
for (Song new_cue_song : songs) {
new_cue_song.set_source(source_);
new_cue_song.set_directory_id(t->dir_id());
new_cue_song.set_directory_id(t->dir());
PerformEBUR128Analysis(new_cue_song);
new_cue_song.set_fingerprint(fingerprint);
@@ -905,7 +901,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
}
bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
const QString &fingerprint,
const SongList &matching_songs,
const QUrl &art_automatic,
@@ -926,7 +922,7 @@ bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
const TagReaderResult result = tagreader_client_->ReadFileBlocking(file, &song_on_disk);
if (result.success() && song_on_disk.is_valid()) {
song_on_disk.set_source(source_);
song_on_disk.set_directory_id(t->dir_id());
song_on_disk.set_directory_id(t->dir());
song_on_disk.set_id(matching_song.id());
PerformEBUR128Analysis(song_on_disk);
song_on_disk.set_fingerprint(fingerprint);
@@ -935,8 +931,6 @@ bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
AddChangedSong(file, matching_song, song_on_disk, t);
}
return result.success() && song_on_disk.is_valid();
}
SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const {
@@ -1205,13 +1199,12 @@ void CollectionWatcher::DirectoryChanged(const QString &subdir) {
void CollectionWatcher::RescanPathsNow() {
const QList<int> dir_ids = rescan_queue_.keys();
for (const int dir_id : dir_ids) {
const QList<int> dirs = rescan_queue_.keys();
for (const int dir : dirs) {
if (stop_or_abort_requested()) break;
ScanTransaction transaction(this, dir_id, false, false, mark_songs_unavailable_);
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
const QStringList paths = rescan_queue_.value(dir_id);
const QStringList paths = rescan_queue_.value(dir);
QMap<QString, quint64> subdir_files_count;
for (const QString &path : paths) {
@@ -1222,14 +1215,11 @@ void CollectionWatcher::RescanPathsNow() {
for (const QString &path : paths) {
if (stop_or_abort_requested()) break;
if (!subdir_mapping_.contains(path)) {
continue;
}
CollectionSubdirectory subdir;
subdir.directory_id = dir_id;
subdir.directory_id = dir;
subdir.mtime = 0;
subdir.path = path;
ScanSubdirectory(subdir_mapping_[path], path, subdir, subdir_files_count[path], &transaction);
ScanSubdirectory(path, subdir, subdir_files_count[path], &transaction);
}
}
@@ -1354,13 +1344,11 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_);
CollectionSubdirectoryList subdirs = transaction.GetAllSubdirs();
const bool has_collection_root_dir = std::any_of(subdirs.begin(), subdirs.end(), [&dir](const CollectionSubdirectory &subdir) { return subdir.path == dir.path; });
if (!has_collection_root_dir) {
qLog(Debug) << "Collection directory wasn't in subdir list, re-adding";
if (subdirs.isEmpty()) {
qLog(Debug) << "Collection directory wasn't in subdir list.";
CollectionSubdirectory subdir;
subdir.directory_id = dir.id;
subdir.path = dir.path;
subdir.mtime = 0;
subdir.directory_id = dir.id;
subdirs << subdir;
}
@@ -1370,7 +1358,7 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
for (const CollectionSubdirectory &subdir : std::as_const(subdirs)) {
if (stop_or_abort_requested()) break;
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
}
}
@@ -1471,8 +1459,6 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
QStringList scanned_paths;
for (const Song &song : songs) {
if (stop_or_abort_requested()) break;
if (!watched_dirs_.contains(song.directory_id())) continue;
const CollectionDirectory dir = watched_dirs_[song.directory_id()];
const QString song_path = song.url().toLocalFile().section(u'/', 0, -2);
if (scanned_paths.contains(song_path)) continue;
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
@@ -1482,7 +1468,7 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
if (subdir.path != song_path) continue;
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
const quint64 files_count = FilesCountForPath(&transaction, subdir.path);
ScanSubdirectory(dir, song_path, subdir, files_count, &transaction);
ScanSubdirectory(song_path, subdir, files_count, &transaction);
scanned_paths << subdir.path;
}
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, 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
@@ -85,7 +85,6 @@ class CollectionWatcher : public QObject {
void SongsReadded(const SongList &songs, const bool unavailable = false);
void SubdirsDiscovered(const CollectionSubdirectoryList &subdirs);
void SubdirsMTimeUpdated(const CollectionSubdirectoryList &subdirs);
void SubdirsDeleted(const CollectionSubdirectoryList &subdirs);
void CompilationsNeedUpdating();
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
void ExitFinished();
@@ -123,7 +122,7 @@ class CollectionWatcher : public QObject {
// Emits the signals for new & deleted songs etc and clears the lists. This causes the new stuff to be updated on UI.
void CommitNewOrUpdatedSongs();
int dir_id() const { return dir_id_; }
int dir() const { return dir_; }
bool is_incremental() const { return incremental_; }
bool ignores_mtime() const { return ignores_mtime_; }
@@ -144,7 +143,7 @@ class CollectionWatcher : public QObject {
quint64 progress_;
quint64 progress_max_;
int dir_id_;
int dir_;
// Incremental scan enters a directory only if it has changed since the last scan.
bool incremental_;
// This type of scan updates every file in a folder that's being scanned.
@@ -180,7 +179,7 @@ class CollectionWatcher : public QObject {
void IncrementalScanNow();
void FullScanNow();
void RescanPathsNow();
void ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
void RescanSongs(const SongList &songs);
private:
@@ -203,7 +202,7 @@ class CollectionWatcher : public QObject {
// Updates the sections of a cue associated and altered (according to mtime) media file during a scan.
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &art_automatic, const SongList &old_cue_songs, ScanTransaction *t) const;
// Updates a single non-cue associated and altered (according to mtime) song during a scan.
bool UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
// Scans a single media file that's present on the disk but not yet in the collection.
// It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file).
SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const;

View File

@@ -34,6 +34,7 @@ constexpr char kShowErrorDialog[] = "show_error_dialog";
constexpr char kStripRemastered[] = "strip_remastered";
constexpr char kSources[] = "sources";
constexpr char kUserToken[] = "user_token";
constexpr char kApiKey[] = "api_key";
} // namespace ScrobblerSettings

View File

@@ -245,6 +245,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void ToggleSearchCoverAuto(const bool checked);
void SaveGeometry();
void Exit();
void DoExit();
void HandleNotificationPreview(const OSDSettings::Type type, const QString &line1, const QString &line2);
@@ -279,7 +280,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
public Q_SLOTS:
void CommandlineOptionsReceived(const QByteArray &string_options);
void Raise();
void Exit();
private:
void SaveSettings();

View File

@@ -690,7 +690,7 @@ bool Song::is_stream() const { return is_radio() || d->source_ == Source::Tidal
bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; }
bool Song::is_cdda() const { return d->source_ == Source::CDDA; }
bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; }
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz; }
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
bool Song::is_module_music() const { return d->filetype_ == FileType::MOD || d->filetype_ == FileType::S3M || d->filetype_ == FileType::XM || d->filetype_ == FileType::IT; }
bool Song::has_cue() const { return !d->cue_path_.isEmpty(); }

View File

@@ -1,175 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2026, 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <cstring>
#include <sys/socket.h>
#include <unistd.h>
#include <csignal>
#include <cerrno>
#include <fcntl.h>
#include <QSocketNotifier>
#include "core/logging.h"
#include "unixsignalwatcher.h"
UnixSignalWatcher *UnixSignalWatcher::sInstance = nullptr;
UnixSignalWatcher::UnixSignalWatcher(QObject *parent)
: QObject(parent),
signal_fd_{-1, -1},
socket_notifier_(nullptr) {
Q_ASSERT(!sInstance);
// Create a socket pair for the self-pipe trick
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, signal_fd_) != 0) {
qLog(Error) << "Failed to create socket pair for signal handling:" << ::strerror(errno);
return;
}
Q_ASSERT(signal_fd_[0] != -1);
// Set the read end to non-blocking mode
// Non-blocking mode is important to prevent HandleSignalNotification from blocking
int flags = ::fcntl(signal_fd_[0], F_GETFL, 0);
if (flags == -1) {
qLog(Error) << "Failed to get socket flags:" << ::strerror(errno);
}
else if (::fcntl(signal_fd_[0], F_SETFL, flags | O_NONBLOCK) == -1) {
qLog(Error) << "Failed to set socket to non-blocking:" << ::strerror(errno);
}
// Set the write end to non-blocking mode as well (used in signal handler)
// Non-blocking mode prevents the signal handler from blocking if buffer is full
flags = ::fcntl(signal_fd_[1], F_GETFL, 0);
if (flags == -1) {
qLog(Error) << "Failed to get socket flags for write end:" << ::strerror(errno);
}
else if (::fcntl(signal_fd_[1], F_SETFL, flags | O_NONBLOCK) == -1) {
qLog(Error) << "Failed to set write end of socket to non-blocking:" << ::strerror(errno);
}
// Set up QSocketNotifier to monitor the read end of the socket
socket_notifier_ = new QSocketNotifier(signal_fd_[0], QSocketNotifier::Read, this);
QObject::connect(socket_notifier_, &QSocketNotifier::activated, this, &UnixSignalWatcher::HandleSignalNotification);
sInstance = this;
}
UnixSignalWatcher::~UnixSignalWatcher() {
if (socket_notifier_) {
socket_notifier_->setEnabled(false);
}
// Restore original signal handlers
for (int i = 0; i < watched_signals_.size(); ++i) {
if (::sigaction(watched_signals_[i], &original_signal_actions_[i], nullptr) != 0) {
qLog(Error) << "Failed to restore signal handler for signal" << watched_signals_[i] << ":" << ::strerror(errno);
}
}
if (signal_fd_[0] != -1) {
::close(signal_fd_[0]);
signal_fd_[0] = -1;
}
if (signal_fd_[1] != -1) {
::close(signal_fd_[1]);
signal_fd_[1] = -1;
}
sInstance = nullptr;
}
void UnixSignalWatcher::WatchForSignal(const int signal) {
// Check if socket pair was created successfully
if (signal_fd_[0] == -1 || signal_fd_[1] == -1) {
qLog(Error) << "Cannot watch for signal: socket pair not initialized";
return;
}
if (watched_signals_.contains(signal)) {
qLog(Error) << "Already watching for signal" << signal;
return;
}
struct sigaction signal_action{};
::memset(&signal_action, 0, sizeof(signal_action));
sigemptyset(&signal_action.sa_mask);
signal_action.sa_handler = UnixSignalWatcher::SignalHandler;
signal_action.sa_flags = SA_RESTART;
struct sigaction old_signal_action{};
::memset(&old_signal_action, 0, sizeof(old_signal_action));
if (::sigaction(signal, &signal_action, &old_signal_action) != 0) {
qLog(Error) << "sigaction error:" << ::strerror(errno);
return;
}
watched_signals_ << signal;
original_signal_actions_ << old_signal_action;
}
void UnixSignalWatcher::SignalHandler(const int signal) {
if (!sInstance || sInstance->signal_fd_[1] == -1) {
return;
}
// Write the signal number to the socket pair (async-signal-safe)
// This is the only operation we perform in the signal handler
// Ignore errors as there's nothing we can safely do about them in a signal handler
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
(void)::write(sInstance->signal_fd_[1], &signal, sizeof(signal));
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
}
void UnixSignalWatcher::HandleSignalNotification() {
// Read all pending signals from the socket
// Multiple signals could arrive before the notifier triggers
while (true) {
int signal = 0;
const ssize_t bytes_read = ::read(signal_fd_[0], &signal, sizeof(signal));
if (bytes_read == sizeof(signal)) {
qLog(Debug) << "Caught signal:" << signal;
Q_EMIT UnixSignal(signal);
}
else if (bytes_read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// No more data available (expected with non-blocking socket)
break;
}
else {
// Error occurred or partial read
break;
}
}
}

View File

@@ -1,53 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2026, 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef UNIXSIGNALWATCHER_H
#define UNIXSIGNALWATCHER_H
#include <csignal>
#include <QObject>
#include <QList>
class QSocketNotifier;
class UnixSignalWatcher : public QObject {
Q_OBJECT
public:
explicit UnixSignalWatcher(QObject *parent = nullptr);
~UnixSignalWatcher() override;
void WatchForSignal(const int signal);
Q_SIGNALS:
void UnixSignal(const int signal);
private:
static void SignalHandler(const int signal);
void HandleSignalNotification();
static UnixSignalWatcher *sInstance;
int signal_fd_[2];
QSocketNotifier *socket_notifier_;
QList<int> watched_signals_;
QList<struct sigaction> original_signal_actions_;
};
#endif // UNIXSIGNALWATCHER_H

View File

@@ -75,7 +75,6 @@ FilesystemDevice::FilesystemDevice(const QUrl &url,
QObject::connect(collection_watcher_, &CollectionWatcher::SongsReadded, &*collection_backend_, &CollectionBackend::MarkSongsUnavailable);
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDiscovered, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDeleted, &*collection_backend_, &CollectionBackend::DeleteSubdirs);
QObject::connect(collection_watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*collection_backend_, &CollectionBackend::CompilationsNeedUpdating);
QObject::connect(collection_watcher_, &CollectionWatcher::UpdateLastSeen, &*collection_backend_, &CollectionBackend::UpdateLastSeen);
QObject::connect(collection_watcher_, &CollectionWatcher::ScanStarted, this, &FilesystemDevice::TaskStarted);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2021, 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
@@ -76,10 +76,6 @@
#include <kdsingleapplication.h>
#ifdef Q_OS_UNIX
#include "core/unixsignalwatcher.h"
#endif
#ifdef HAVE_QTSPARKLE
# include <qtsparkle-qt6/Updater>
#endif // HAVE_QTSPARKLE
@@ -369,12 +365,6 @@ int main(int argc, char *argv[]) {
#endif
options);
#ifdef Q_OS_UNIX
UnixSignalWatcher unix_signal_watcher;
unix_signal_watcher.WatchForSignal(SIGTERM);
QObject::connect(&unix_signal_watcher, &UnixSignalWatcher::UnixSignal, &w, &MainWindow::Exit);
#endif
#ifdef Q_OS_MACOS
mac::EnableFullScreen(w);
#endif // Q_OS_MACOS

View File

@@ -152,6 +152,7 @@ void PlaylistContainer::SetActions(QAction *new_playlist, QAction *load_playlist
QObject::connect(clear_playlist, &QAction::triggered, this, &PlaylistContainer::ClearPlaylist);
QObject::connect(next_playlist, &QAction::triggered, this, &PlaylistContainer::GoToNextPlaylistTab);
QObject::connect(previous_playlist, &QAction::triggered, this, &PlaylistContainer::GoToPreviousPlaylistTab);
QObject::connect(clear_playlist, &QAction::triggered, this, &PlaylistContainer::ClearPlaylist);
QObject::connect(save_all_playlists, &QAction::triggered, &*manager_, &PlaylistManager::SaveAllPlaylists);
}

View File

@@ -42,15 +42,22 @@
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/settings.h"
#include "constants/scrobblersettings.h"
#include "lastfmimport.h"
#include "lastfmscrobbler.h"
using namespace Qt::Literals::StringLiterals;
using namespace ScrobblerSettings;
namespace {
constexpr int kRequestsDelay = 2000;
constexpr int kMaxRetries = 5;
constexpr int kInitialBackoffMs = 5000;
constexpr int kMaxBackoffShift = 10; // Maximum shift value to prevent overflow
constexpr int kRetryHttpStatusCode1 = 500; // Internal Server Error
constexpr int kRetryHttpStatusCode2 = 503; // Service Unavailable
}
LastFMImport::LastFMImport(const SharedPtr<NetworkAccessManager> network, QObject *parent)
@@ -101,14 +108,17 @@ void LastFMImport::ReloadSettings() {
Settings s;
s.beginGroup(LastFMScrobbler::kSettingsGroup);
username_ = s.value("username").toString();
api_key_ = s.value(ScrobblerSettings::kApiKey).toString();
s.endGroup();
}
QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
const QString api_key = !api_key_.isEmpty() ? api_key_ : QLatin1String(LastFMScrobbler::kApiKey);
ParamList params = ParamList()
<< Param(u"api_key"_s, QLatin1String(LastFMScrobbler::kApiKey))
<< Param(u"api_key"_s, api_key)
<< Param(u"user"_s, username_)
<< Param(u"lang"_s, QLocale().name().left(2).toLower())
<< Param(u"format"_s, u"json"_s)
@@ -234,11 +244,11 @@ void LastFMImport::SendGetRecentTracksRequest(GetRecentTracksRequest request) {
}
QNetworkReply *reply = CreateRequest(params);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetRecentTracksRequestFinished(reply, request.page); });
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetRecentTracksRequestFinished(reply, request); });
}
void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const int page) {
void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, GetRecentTracksRequest request) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
@@ -247,10 +257,21 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in
const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (!json_object_result.success()) {
if (ShouldRetryRequest(json_object_result) && request.retry_count < kMaxRetries) {
const int delay_ms = CalculateBackoffDelay(request.retry_count);
LogRetryAttempt(json_object_result.http_status_code, request.retry_count, delay_ms);
QTimer::singleShot(delay_ms, this, [this, request]() {
GetRecentTracksRequest retry_request(request.page, request.retry_count + 1);
SendGetRecentTracksRequest(retry_request);
});
return;
}
Error(json_object_result.error_message);
return;
}
const int page = request.page;
QJsonObject json_object = json_object_result.json_object;
if (json_object.isEmpty()) {
return;
@@ -390,11 +411,11 @@ void LastFMImport::SendGetTopTracksRequest(GetTopTracksRequest request) {
}
QNetworkReply *reply = CreateRequest(params);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetTopTracksRequestFinished(reply, request.page); });
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetTopTracksRequestFinished(reply, request); });
}
void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int page) {
void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTracksRequest request) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
@@ -403,10 +424,21 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (!json_object_result.success()) {
if (ShouldRetryRequest(json_object_result) && request.retry_count < kMaxRetries) {
const int delay_ms = CalculateBackoffDelay(request.retry_count);
LogRetryAttempt(json_object_result.http_status_code, request.retry_count, delay_ms);
QTimer::singleShot(delay_ms, this, [this, request]() {
GetTopTracksRequest retry_request(request.page, request.retry_count + 1);
SendGetTopTracksRequest(retry_request);
});
return;
}
Error(json_object_result.error_message);
return;
}
const int page = request.page;
QJsonObject json_object = json_object_result.json_object;
if (json_object.isEmpty()) {
return;
@@ -516,6 +548,23 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
}
bool LastFMImport::ShouldRetryRequest(const JsonObjectResult &result) const {
return result.http_status_code == kRetryHttpStatusCode1 ||
result.http_status_code == kRetryHttpStatusCode2 ||
result.network_error == QNetworkReply::TemporaryNetworkFailureError;
}
void LastFMImport::LogRetryAttempt(const int http_status_code, const int retry_count, const int delay_ms) const {
qLog(Warning) << "Last.fm request failed with status" << http_status_code
<< ", retrying in" << delay_ms << "ms (attempt"
<< (retry_count + 1) << "of" << kMaxRetries << ")";
}
int LastFMImport::CalculateBackoffDelay(const int retry_count) const {
const int safe_shift = std::min(retry_count, kMaxBackoffShift);
return kInitialBackoffMs * (1 << safe_shift);
}
void LastFMImport::UpdateTotalCheck() {
Q_EMIT UpdateTotal(lastplayed_total_, playcount_total_);

View File

@@ -60,12 +60,14 @@ class LastFMImport : public JsonBaseRequest {
using ParamList = QList<Param>;
struct GetRecentTracksRequest {
explicit GetRecentTracksRequest(const int _page) : page(_page) {}
explicit GetRecentTracksRequest(const int _page, const int _retry_count = 0) : page(_page), retry_count(_retry_count) {}
int page;
int retry_count;
};
struct GetTopTracksRequest {
explicit GetTopTracksRequest(const int _page) : page(_page) {}
explicit GetTopTracksRequest(const int _page, const int _retry_count = 0) : page(_page), retry_count(_retry_count) {}
int page;
int retry_count;
};
private:
@@ -78,6 +80,10 @@ class LastFMImport : public JsonBaseRequest {
void SendGetRecentTracksRequest(GetRecentTracksRequest request);
void SendGetTopTracksRequest(GetTopTracksRequest request);
bool ShouldRetryRequest(const JsonObjectResult &result) const;
int CalculateBackoffDelay(const int retry_count) const;
void LogRetryAttempt(const int http_status_code, const int retry_count, const int delay_ms) const;
void Error(const QString &error, const QVariant &debug = QVariant()) override;
void UpdateTotalCheck();
@@ -95,14 +101,15 @@ class LastFMImport : public JsonBaseRequest {
private Q_SLOTS:
void FlushRequests();
void GetRecentTracksRequestFinished(QNetworkReply *reply, const int page);
void GetTopTracksRequestFinished(QNetworkReply *reply, const int page);
void GetRecentTracksRequestFinished(QNetworkReply *reply, GetRecentTracksRequest request);
void GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTracksRequest request);
private:
SharedPtr<NetworkAccessManager> network_;
QTimer *timer_flush_requests_;
QString username_;
QString api_key_;
bool lastplayed_;
bool playcount_;
int playcount_total_;

View File

@@ -113,6 +113,7 @@ void LastFMScrobbler::ReloadSettings() {
s.beginGroup(kSettingsGroup);
enabled_ = s.value(ScrobblerSettings::kEnabled, false).toBool();
api_key_ = s.value(ScrobblerSettings::kApiKey).toString();
s.endGroup();
s.beginGroup(ScrobblerSettings::kSettingsGroup);

View File

@@ -64,6 +64,7 @@ class LastFMScrobbler : public ScrobblerService {
bool subscriber() const { return subscriber_; }
bool submitted() const override { return submitted_; }
QString username() const { return username_; }
QString api_key() const { return api_key_; }
void Authenticate();
void UpdateNowPlaying(const Song &song) override;
@@ -139,6 +140,7 @@ class LastFMScrobbler : public ScrobblerService {
bool subscriber_;
QString username_;
QString session_key_;
QString api_key_;
bool submitted_;
Song song_playing_;

View File

@@ -106,6 +106,7 @@ void ScrobblerSettingsPage::Load() {
ui_->checkbox_source_unknown->setChecked(scrobbler_->sources().contains(Song::Source::Unknown));
ui_->checkbox_lastfm_enable->setChecked(lastfmscrobbler_->enabled());
ui_->lineedit_lastfm_api_key->setText(lastfmscrobbler_->api_key());
LastFM_RefreshControls(lastfmscrobbler_->authenticated());
ui_->checkbox_listenbrainz_enable->setChecked(listenbrainzscrobbler_->enabled());
@@ -152,6 +153,7 @@ void ScrobblerSettingsPage::Save() {
s.beginGroup(LastFMScrobbler::kSettingsGroup);
s.setValue(kEnabled, ui_->checkbox_lastfm_enable->isChecked());
s.setValue(kApiKey, ui_->lineedit_lastfm_api_key->text());
s.endGroup();
s.beginGroup(ListenBrainzScrobbler::kSettingsGroup);

View File

@@ -234,6 +234,43 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_lastfm_api_key">
<item>
<widget class="QLabel" name="label_lastfm_api_key">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>API key:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineedit_lastfm_api_key">
<property name="placeholderText">
<string>Optional - your own Last.fm API key</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_lastfm_api_key_info">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Using your own API key can help avoid rate limiting for large libraries. Get one at &lt;/span&gt;&lt;a href=&quot;https://www.last.fm/api/account/create&quot;&gt;&lt;span style=&quot; font-size:8pt; text-decoration: underline; color:#0000ff;&quot;&gt;https://www.last.fm/api/account/create&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="widget_lastfm_login_state" native="true"/>
</item>
@@ -394,6 +431,7 @@
<tabstop>checkbox_source_somafm</tabstop>
<tabstop>checkbox_source_radioparadise</tabstop>
<tabstop>checkbox_lastfm_enable</tabstop>
<tabstop>lineedit_lastfm_api_key</tabstop>
<tabstop>button_lastfm_login</tabstop>
<tabstop>checkbox_listenbrainz_enable</tabstop>
<tabstop>lineedit_listenbrainz_user_token</tabstop>

View File

@@ -1227,7 +1227,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Don&apos;t show in various artists</source>
<translation>Όχι εμφάνιση σε διάφορους καλλιτέχνες</translation>
<translation type="unfinished">Don&apos;t show in various artists</translation>
</message>
<message>
<source>There are other songs in this album</source>
@@ -1726,7 +1726,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Set through album cover search (%1)</source>
<translation>Ορισμός μέσω αναζήτησης εξωφύλλου άλμπουμ (%1)</translation>
<translation type="unfinished">Set through album cover search (%1)</translation>
</message>
<message>
<source>Automatically picked up from album directory (%1)</source>
@@ -1741,7 +1741,7 @@ If there are no matches then it will use the largest image in the directory.</so
<name>CueParser</name>
<message>
<source>Saving CUE files is not supported.</source>
<translation>Η αποθήκευση αρχείων CUE δεν υποστηρίζεται.</translation>
<translation type="unfinished">Saving CUE files is not supported.</translation>
</message>
</context>
<context>
@@ -2180,7 +2180,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Different art across multiple songs.</source>
<translation>Διαφορετική τέχνη σε πολλαπλά τραγούδια.</translation>
<translation type="unfinished">Different art across multiple songs.</translation>
</message>
<message>
<source>Previous</source>
@@ -3416,7 +3416,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Toggle skip status</source>
<translation>Εναλλαγή κατάστασης παράκαμψης</translation>
<translation type="unfinished">Toggle skip status</translation>
</message>
<message>
<source>Rescan song(s)...</source>
@@ -3464,7 +3464,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>You are running Strawberry under Rosetta. Running Strawberry under Rosetta is unsupported and known to have issues. You should download Strawberry for the correct CPU architecture from %1</source>
<translation>Τρέχετε το Strawberry κάτω από Rosetta. Η χρήση του Strawberry κάτω από Rosetta δεν υποστηρίζεται και είναι γνωστό ότι παρουσιάζει προβλήματα. Θα πρέπει να λάβετε το Strawberry για τη σωστή αρχιτεκτονική CPU από %1</translation>
<translation type="unfinished">You are running Strawberry under Rosetta. Running Strawberry under Rosetta is unsupported and known to have issues. You should download Strawberry for the correct CPU architecture from %1</translation>
</message>
<message>
<source>Sponsoring Strawberry</source>
@@ -4351,7 +4351,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Loudness Range</source>
<translation>Loudness Range</translation>
<translation type="unfinished">Loudness Range</translation>
</message>
</context>
<context>
@@ -4501,7 +4501,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Couldn&apos;t create playlist</source>
<translation>Αδυναμία δημιουργίας λίστας αναπαραγωγής</translation>
<translation type="unfinished">Couldn&apos;t create playlist</translation>
</message>
<message>
<source>Save playlist</source>
@@ -4766,11 +4766,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Warn me when closing a playlist tab</source>
<translation>Προειδοποίηση κατά το κλείσιμο μιας καρτέλας λίστας αναπαραγωγής</translation>
<translation type="unfinished">Warn me when closing a playlist tab</translation>
</message>
<message>
<source>This option can be changed in the &quot;Behavior&quot; preferences</source>
<translation/>
<translation type="unfinished">This option can be changed in the &quot;Behavior&quot; preferences</translation>
</message>
<message>
<source>Double-click here to favorite this playlist so it will be saved and remain accessible through the &quot;Playlists&quot; panel on the left side bar</source>
@@ -4888,7 +4888,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Loads files/URLs, replacing current playlist</source>
<translation>Φόρτωση αρχείων/URL, αντικατάσταση της τρέχουσας λίστας αναπαραγωγής</translation>
<translation type="unfinished">Loads files/URLs, replacing current playlist</translation>
</message>
<message>
<source>Play the &lt;n&gt;th track in the playlist</source>
@@ -4940,11 +4940,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Destination file %1 exists, but not allowed to overwrite.</source>
<translation>Το αρχείο προορισμού %1 υπάρχει, αλλά δεν επιτρέπεται να αντικατασταθεί.</translation>
<translation type="unfinished">Destination file %1 exists, but not allowed to overwrite.</translation>
</message>
<message>
<source>Destination file %1 exists, but not allowed to overwrite</source>
<translation>Το αρχείο προορισμού %1 υπάρχει, αλλά δεν επιτρέπεται να αντικατασταθεί</translation>
<translation type="unfinished">Destination file %1 exists, but not allowed to overwrite</translation>
</message>
<message>
<source>Could not copy file %1 to %2.</source>
@@ -5012,7 +5012,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>%1 songs in %2 different directories selected, are you sure you want to open them all?</source>
<translation>%1 τραγούδια σε διαφορετικούς καταλόγους %2 επιλέχθηκαν, είστε σίγουροι ότι θέλετε να τα ανοίξετε όλα;</translation>
<translation type="unfinished">%1 songs in %2 different directories selected, are you sure you want to open them all?</translation>
</message>
<message>
<source>Failed to load image from data for %1</source>
@@ -5315,11 +5315,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Receiving album cover for %1 album...</source>
<translation>Λήψη εξωφύλλου για το άλμπουμ %1...</translation>
<translation type="unfinished">Receiving album cover for %1 album...</translation>
</message>
<message>
<source>Receiving album covers for %1 albums...</source>
<translation>Λήψη εξώφυλλων για άλμπουμ %1...</translation>
<translation type="unfinished">Receiving album covers for %1 albums...</translation>
</message>
<message>
<source>No match.</source>
@@ -5472,7 +5472,7 @@ Are you sure you want to continue?</source>
<message numerus="yes">
<source>%n track(s)</source>
<translation type="unfinished">
<numerusform>%n κομμάτι(α)</numerusform>
<numerusform>%n track(s)</numerusform>
<numerusform>%n track(s)</numerusform>
</translation>
</message>
@@ -5493,15 +5493,15 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Move up</source>
<translation>Μετακίνηση πάνω</translation>
<translation type="unfinished">Move up</translation>
</message>
<message>
<source>Ctrl+Down</source>
<translation>Ctrl+Down</translation>
<translation type="unfinished">Ctrl+Down</translation>
</message>
<message>
<source>Remove</source>
<translation>Αφαίρεση</translation>
<translation type="unfinished">Remove</translation>
</message>
<message>
<source>Clear</source>
@@ -5516,26 +5516,26 @@ Are you sure you want to continue?</source>
<name>RadioParadiseService</name>
<message>
<source>Getting %1 channels</source>
<translation>Λήψη %1 καναλιών</translation>
<translation type="unfinished">Getting %1 channels</translation>
</message>
</context>
<context>
<name>RadioView</name>
<message>
<source>Append to current playlist</source>
<translation>Προσάρτηση στην τρέχουσα λίστα</translation>
<translation type="unfinished">Append to current playlist</translation>
</message>
<message>
<source>Replace current playlist</source>
<translation>Αντικατάσταση της τρέχουσας λίστας</translation>
<translation type="unfinished">Replace current playlist</translation>
</message>
<message>
<source>Open in new playlist</source>
<translation>Άνοιγμα σε νέα λίστα</translation>
<translation type="unfinished">Open in new playlist</translation>
</message>
<message>
<source>Open homepage</source>
<translation>Άνοιγμα αρχικής σελίδας</translation>
<translation type="unfinished">Open homepage</translation>
</message>
<message>
<source>Donate</source>
@@ -5557,7 +5557,7 @@ Are you sure you want to continue?</source>
<name>SavePlaylistsDialog</name>
<message>
<source>Select directory for saving playlists</source>
<translation>Επιλέξτε φάκελο για τις λίστες αναπαραγωγής</translation>
<translation type="unfinished">Select directory for saving playlists</translation>
</message>
<message>
<source>Type</source>
@@ -5711,11 +5711,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Show love button</source>
<translation>Εμφάνιση πλήκτρου αγάπης</translation>
<translation type="unfinished">Show love button</translation>
</message>
<message>
<source>Submit scrobbles every</source>
<translation>Υποβολή scrobbles κάθε</translation>
<translation type="unfinished">Submit scrobbles every</translation>
</message>
<message>
<source> seconds</source>
@@ -5811,7 +5811,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Offline mode (Only cache scrobbles)</source>
<translation>Λειτουργία εκτός σύνδεσης (Μόνο cache scrobbles)</translation>
<translation type="unfinished">Offline mode (Only cache scrobbles)</translation>
</message>
<message>
<source>This is the delay between when a song is scrobbled and when scrobbles are submitted to the server. Setting the time to 0 seconds will submit scrobbles immediately.</source>
@@ -5830,11 +5830,11 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Open URL in web browser?</source>
<translation>Άνοιγμα διεύθυνσης URL στο πρόγραμμα περιήγησης;</translation>
<translation type="unfinished">Open URL in web browser?</translation>
</message>
<message>
<source>Press &quot;Save&quot; to copy the URL to clipboard and manually open it in a web browser.</source>
<translation>Πατήστε &quot;Save&quot; για να αντιγράψετε το URL στο πρόχειρο και να το ανοίξετε χειροκίνητα σε ένα πρόγραμμα περιήγησης.</translation>
<translation type="unfinished">Press &quot;Save&quot; to copy the URL to clipboard and manually open it in a web browser.</translation>
</message>
<message>
<source>Could not open URL. Please open this URL in your browser</source>
@@ -5842,19 +5842,19 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Invalid reply from web browser. Missing token.</source>
<translation>Μη έγκυρη απάντηση από το πρόγραμμα περιήγησης. Λείπει token.</translation>
<translation type="unfinished">Invalid reply from web browser. Missing token.</translation>
</message>
<message>
<source>Received invalid reply from web browser. Try another browser.</source>
<translation>Λήφθηκε μη έγκυρη απάντηση από το πρόγραμμα περιήγησης. Δοκιμάστε ένα άλλο πρόγραμμα περιήγησης.</translation>
<translation type="unfinished">Received invalid reply from web browser. Try another browser.</translation>
</message>
<message>
<source>Scrobbler %1 is not authenticated!</source>
<translation>Το Scrobbler %1 δεν είναι πιστοποιημένο!</translation>
<translation type="unfinished">Scrobbler %1 is not authenticated!</translation>
</message>
<message>
<source>Scrobbler %1 error: %2</source>
<translation>Scrobbler %1 σφάλμα: %2</translation>
<translation type="unfinished">Scrobbler %1 error: %2</translation>
</message>
</context>
<context>
@@ -5873,7 +5873,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Streaming</source>
<translation>Streaming</translation>
<translation type="unfinished">Streaming</translation>
</message>
</context>
<context>
@@ -6034,14 +6034,14 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Choose a name for your smart playlist</source>
<translation>Επιλέξτε ένα όνομα για την έξυπνη λίστα αναπαραγωγής σας</translation>
<translation type="unfinished">Choose a name for your smart playlist</translation>
</message>
</context>
<context>
<name>SmartPlaylistWizardFinishPage</name>
<message>
<source>Form</source>
<translation>Φόρμα</translation>
<translation type="unfinished">Form</translation>
</message>
<message>
<source>Name</source>
@@ -6049,7 +6049,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Use dynamic mode</source>
<translation>Χρήση δυναμικής λειτουργίας</translation>
<translation type="unfinished">Use dynamic mode</translation>
</message>
<message>
<source>In dynamic mode new tracks will be chosen and added to the playlist every time a song finishes.</source>

View File

@@ -119,11 +119,11 @@
</message>
<message>
<source>Failed to open cover file %1 for reading: %2</source>
<translation>No se pudo abrir el archivo de portada %1 para lectura: %2</translation>
<translation type="unfinished">Failed to open cover file %1 for reading: %2</translation>
</message>
<message>
<source>Cover file %1 is empty.</source>
<translation>El archivo de portada %1 está vacío.</translation>
<translation type="unfinished">Cover file %1 is empty.</translation>
</message>
<message>
<source>unknown</source>
@@ -135,27 +135,27 @@
</message>
<message>
<source>Failed to open cover file %1 for writing: %2</source>
<translation>No se pudo abrir el archivo de portada %1 para escribir: %2</translation>
<translation type="unfinished">Failed to open cover file %1 for writing: %2</translation>
</message>
<message>
<source>Failed writing cover to file %1: %2</source>
<translation>Error al escribir la portada en el archivo %1: %2</translation>
<translation type="unfinished">Failed writing cover to file %1: %2</translation>
</message>
<message>
<source>Failed writing cover to file %1.</source>
<translation>Error al escribir la portada en el archivo %1.</translation>
<translation type="unfinished">Failed writing cover to file %1.</translation>
</message>
<message>
<source>Failed to delete cover file %1: %2</source>
<translation>No se pudo eliminar el archivo de portada %1: %2</translation>
<translation type="unfinished">Failed to delete cover file %1: %2</translation>
</message>
<message>
<source>Failed to write cover to file %1: %2</source>
<translation>No se pudo escribir la portada en el archivo %1: %2</translation>
<translation type="unfinished">Failed to write cover to file %1: %2</translation>
</message>
<message>
<source>Could not save cover to file %1.</source>
<translation>No se pudo guardar la portada en el archivo %1.</translation>
<translation type="unfinished">Could not save cover to file %1.</translation>
</message>
</context>
<context>
@@ -273,7 +273,7 @@
</message>
<message>
<source>Could not save cover to file %1.</source>
<translation>No se pudo guardar la portada en el archivo %1.</translation>
<translation type="unfinished">Could not save cover to file %1.</translation>
</message>
</context>
<context>
@@ -339,7 +339,7 @@
</message>
<message>
<source>Turbine</source>
<translation>Turbina</translation>
<translation type="unfinished">Turbine</translation>
</message>
<message>
<source>Sonogram</source>
@@ -549,7 +549,7 @@
</message>
<message>
<source>p&amp;lughw</source>
<translation>enchufe</translation>
<translation type="unfinished">p&amp;lughw</translation>
</message>
<message>
<source>pcm</source>
@@ -569,7 +569,7 @@
</message>
<message>
<source>Upmix / downmix to</source>
<translation>Mezcla ascendente/descendente a</translation>
<translation type="unfinished">Upmix / downmix to</translation>
</message>
<message>
<source>channels</source>
@@ -577,15 +577,15 @@
</message>
<message>
<source>Improve headphone listening of stereo audio records (bs2b)</source>
<translation>Mejorar la escucha de grabaciones de audio estéreo con auriculares (B2B)</translation>
<translation type="unfinished">Improve headphone listening of stereo audio records (bs2b)</translation>
</message>
<message>
<source>Enable HTTP/2 for streaming</source>
<translation>Habilitar HTTP/2 para transmisión</translation>
<translation type="unfinished">Enable HTTP/2 for streaming</translation>
</message>
<message>
<source>Use strict SSL mode</source>
<translation>Utilice el modo SSL estricto</translation>
<translation type="unfinished">Use strict SSL mode</translation>
</message>
<message>
<source>Buffer</source>
@@ -649,19 +649,19 @@
</message>
<message>
<source>Fallback-gain</source>
<translation>Ganancia de respaldo</translation>
<translation type="unfinished">Fallback-gain</translation>
</message>
<message>
<source>EBU R 128 Loudness Normalization</source>
<translation>Normalización de sonoridad EBU R 128</translation>
<translation type="unfinished">EBU R 128 Loudness Normalization</translation>
</message>
<message>
<source>Perform track loudness normalization</source>
<translation>Realizar la normalización de la sonoridad de la pista</translation>
<translation type="unfinished">Perform track loudness normalization</translation>
</message>
<message>
<source>Target Level</source>
<translation>Nivel objetivo</translation>
<translation type="unfinished">Target Level</translation>
</message>
<message>
<source>Fading</source>
@@ -712,7 +712,7 @@
</message>
<message>
<source>Show song progress on taskbar</source>
<translation>Mostrar el progreso de la canción en la barra de tareas</translation>
<translation type="unfinished">Show song progress on taskbar</translation>
</message>
<message>
<source>Resume playback on start</source>
@@ -850,15 +850,15 @@
<name>CollectionBackend</name>
<message>
<source>Unable to execute collection SQL query: %1</source>
<translation>No se puede ejecutar la consulta SQL de recopilación: %1</translation>
<translation type="unfinished">Unable to execute collection SQL query: %1</translation>
</message>
<message>
<source>Failed SQL query: %1</source>
<translation>Consulta SQL fallida: %1</translation>
<translation type="unfinished">Failed SQL query: %1</translation>
</message>
<message>
<source>Updating %1 database.</source>
<translation>Actualizando la base de datos %1.</translation>
<translation type="unfinished">Updating %1 database.</translation>
</message>
</context>
<context>
@@ -873,7 +873,7 @@
</message>
<message>
<source>MenuPopupToolButton</source>
<translation>Botón de herramienta emergente de menú</translation>
<translation type="unfinished">MenuPopupToolButton</translation>
</message>
<message>
<source>Entire collection</source>
@@ -992,7 +992,7 @@
<name>CollectionLibrary</name>
<message>
<source>Saving playcounts and ratings</source>
<translation>Guardar recuentos de reproducciones y calificaciones</translation>
<translation type="unfinished">Saving playcounts and ratings</translation>
</message>
</context>
<context>
@@ -1050,11 +1050,11 @@
</message>
<message>
<source>Perform song EBU R 128 analysis (required for EBU R 128 loudness normalization)</source>
<translation>Realizar el análisis de la canción EBU R 128 (necesario para la normalización de la sonoridad EBU R 128)</translation>
<translation type="unfinished">Perform song EBU R 128 analysis (required for EBU R 128 loudness normalization)</translation>
</message>
<message>
<source>Expire unavailable songs after</source>
<translation>Las canciones no disponibles expirarán después de</translation>
<translation type="unfinished">Expire unavailable songs after</translation>
</message>
<message>
<source>days</source>
@@ -1087,11 +1087,11 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Use various artists for compilation albums</source>
<translation>Utilice varios artistas para álbumes recopilatorios</translation>
<translation type="unfinished">Use various artists for compilation albums</translation>
</message>
<message>
<source>Skip leading articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;) when sorting artist names</source>
<translation>Omitir los artículos iniciales ("él", "un", "una") al ordenar los nombres de los artistas</translation>
<translation type="unfinished">Skip leading articles (&quot;the&quot;, &quot;a&quot;, &quot;an&quot;) when sorting artist names</translation>
</message>
<message>
<source>Album cover pixmap cache</source>
@@ -1119,27 +1119,27 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Song playcounts and ratings</source>
<translation>Número de reproducciones y calificaciones de canciones</translation>
<translation type="unfinished">Song playcounts and ratings</translation>
</message>
<message>
<source>Save playcounts to song tags when possible</source>
<translation>Guarde los recuentos de reproducciones en las etiquetas de las canciones cuando sea posible</translation>
<translation type="unfinished">Save playcounts to song tags when possible</translation>
</message>
<message>
<source>Save ratings to song tags when possible</source>
<translation>Guarde las calificaciones en las etiquetas de las canciones cuando sea posible</translation>
<translation type="unfinished">Save ratings to song tags when possible</translation>
</message>
<message>
<source>Overwrite database playcount when songs are re-read from disk</source>
<translation>Sobrescribir el recuento de reproducciones de la base de datos cuando se vuelven a leer las canciones desde el disco</translation>
<translation type="unfinished">Overwrite database playcount when songs are re-read from disk</translation>
</message>
<message>
<source>Overwrite database rating when songs are re-read from disk</source>
<translation>Sobrescribir la clasificación de la base de datos cuando se vuelven a leer las canciones desde el disco</translation>
<translation type="unfinished">Overwrite database rating when songs are re-read from disk</translation>
</message>
<message>
<source>Save playcounts and ratings to files now</source>
<translation>Guarda recuentos de reproducciones y calificaciones en archivos ahora</translation>
<translation type="unfinished">Save playcounts and ratings to files now</translation>
</message>
<message>
<source>Enable delete files in the right click context menu</source>
@@ -1151,7 +1151,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Write all playcounts and ratings to files</source>
<translation>Escribe todos los recuentos de reproducción y calificaciones en archivos</translation>
<translation type="unfinished">Write all playcounts and ratings to files</translation>
</message>
<message>
<source>Are you sure you want to write song playcounts and ratings to file for all songs in your collection?</source>
@@ -1238,7 +1238,7 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>Error</source>
<translation>Error</translation>
<translation type="unfinished">Error</translation>
</message>
<message>
<source>None of the selected songs were suitable for copying to a device</source>
@@ -1461,11 +1461,11 @@ If there are no matches then it will use the largest image in the directory.</so
</message>
<message>
<source>EBU R 128 Integrated Loudness</source>
<translation>EBU R 128 Sonoridad integrada</translation>
<translation type="unfinished">EBU R 128 Integrated Loudness</translation>
</message>
<message>
<source>EBU R 128 Loudness Range</source>
<translation/>
<translation type="unfinished">EBU R 128 Loudness Range</translation>
</message>
<message>
<source>Show album cover</source>

View File

@@ -9,7 +9,7 @@
</message>
<message>
<source>About Strawberry</source>
<translation>Rakenduse teave: Strawberry</translation>
<translation>Strawberry teave</translation>
</message>
<message>
<source>Version %1</source>
@@ -45,7 +45,7 @@
</message>
<message>
<source>Contributors</source>
<translation>Kaasautorid</translation>
<translation>Toetajad</translation>
</message>
<message>
<source>Clementine authors</source>
@@ -53,7 +53,7 @@
</message>
<message>
<source>Clementine contributors</source>
<translation>Clementine'i kaasautorid</translation>
<translation>Clementine'i toetajad</translation>
</message>
<message>
<source>Thanks to</source>
@@ -233,7 +233,7 @@
</message>
<message>
<source>Really cancel?</source>
<translation>Kas tõesti katkestame?</translation>
<translation>Kas tühistada?</translation>
</message>
<message>
<source>Closing this window will stop searching for album covers.</source>

View File

@@ -9,7 +9,7 @@
</message>
<message>
<source>About Strawberry</source>
<translation>О программе Strawberry</translation>
<translation>О Strawberry</translation>
</message>
<message>
<source>Version %1</source>
@@ -95,7 +95,7 @@
</message>
<message>
<source>Unset cover</source>
<translation>Сбросить обложку</translation>
<translation>Удалить обложку</translation>
</message>
<message>
<source>Delete cover</source>
@@ -621,11 +621,11 @@
</message>
<message>
<source>Replay Gain</source>
<translation>Нормализация громкости (Replay Gain)</translation>
<translation>Нормализация громкости</translation>
</message>
<message>
<source>Use Replay Gain metadata if it is available</source>
<translation>Использовать метаданные нормализации (Replay Gain) по возможности</translation>
<translation>Использовать метаданные нормализации по возможности</translation>
</message>
<message>
<source>Replay Gain mode</source>
@@ -657,7 +657,7 @@
</message>
<message>
<source>Perform track loudness normalization</source>
<translation>Нормализовать громкость дорожки</translation>
<translation>Выполнять нормализацию громкости дорожки</translation>
</message>
<message>
<source>Target Level</source>
@@ -776,7 +776,7 @@
</message>
<message>
<source>Pressing &quot;Previous&quot; in player will...</source>
<translation>Нажатие кнопки «Предыдущий» выполнит</translation>
<translation>Нажатие кнопки «Предыдущий» осуществит</translation>
</message>
<message>
<source>Jump to previous song right away</source>
@@ -6078,7 +6078,7 @@ Are you sure you want to continue?</source>
</message>
<message>
<source>Ever played</source>
<translation>Когда-либо прослушивались</translation>
<translation>Любые прослушанные</translation>
</message>
<message>
<source>Never played</source>