Compare commits
11 Commits
copilot/fi
...
l10n_maste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
610b458196 | ||
|
|
ad285a91f2 | ||
|
|
6400f903e8 | ||
|
|
83d5f3d8f2 | ||
|
|
582b8e8076 | ||
|
|
030908f6ac | ||
|
|
34ae443548 | ||
|
|
1c9e99e776 | ||
|
|
4e6459b977 | ||
|
|
d2b5359fa9 | ||
|
|
1d82977441 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -747,7 +747,7 @@ jobs:
|
||||
df -h
|
||||
- name: Build FreeBSD
|
||||
id: build-freebsd
|
||||
uses: vmactions/freebsd-vm@v1.3.5
|
||||
uses: vmactions/freebsd-vm@v1.3.7
|
||||
with:
|
||||
usesh: true
|
||||
mem: 8192
|
||||
|
||||
@@ -84,8 +84,6 @@ 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
|
||||
@@ -255,7 +253,6 @@ find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
|
||||
|
||||
if(APPLE)
|
||||
find_library(SPARKLE Sparkle)
|
||||
#find_package(SPMediaKeyTap REQUIRED)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
@@ -1218,6 +1215,10 @@ 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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -537,10 +537,24 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (subdir.mtime == 0) {
|
||||
// Delete the subdirectory
|
||||
// See if this subdirectory already exists in the database
|
||||
bool exists = false;
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
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.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
@@ -549,42 +563,36 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -252,6 +252,7 @@ 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);
|
||||
|
||||
@@ -124,6 +124,7 @@ 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);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -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_(dir),
|
||||
dir_id_(dir),
|
||||
incremental_(incremental),
|
||||
ignores_mtime_(ignores_mtime),
|
||||
mark_songs_unavailable_(mark_songs_unavailable),
|
||||
@@ -313,6 +313,19 @@ 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);
|
||||
@@ -338,34 +351,24 @@ 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_)) {
|
||||
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
|
||||
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
||||
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_id_], 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_)) {
|
||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
|
||||
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_id_], subdir.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
new_subdirs.clear();
|
||||
|
||||
if (incremental_ || ignores_mtime_) {
|
||||
Q_EMIT watcher_->UpdateLastSeen(dir_, expire_unavailable_songs_days_);
|
||||
Q_EMIT watcher_->UpdateLastSeen(dir_id_, expire_unavailable_songs_days_);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -374,7 +377,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
|
||||
|
||||
if (cached_songs_dirty_) {
|
||||
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
|
||||
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_id_);
|
||||
for (const Song &song : songs) {
|
||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
cached_songs_.insert(p, song);
|
||||
@@ -393,7 +396,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_);
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_id_);
|
||||
for (const Song &song : songs) {
|
||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
cached_songs_missing_fingerprint_.insert(p, song);
|
||||
@@ -408,7 +411,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_);
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_id_);
|
||||
for (const Song &song : songs) {
|
||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
cached_songs_missing_loudness_characteristics_.insert(p, song);
|
||||
@@ -430,7 +433,7 @@ void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const CollectionSubdire
|
||||
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||
}
|
||||
|
||||
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
|
||||
@@ -440,7 +443,7 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||
}
|
||||
|
||||
CollectionSubdirectoryList ret;
|
||||
@@ -457,7 +460,7 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdi
|
||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||
}
|
||||
|
||||
return known_subdirs_;
|
||||
@@ -494,7 +497,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.path, CollectionSubdirectory(), files_count, &transaction);
|
||||
ScanSubdirectory(dir, dir.path, CollectionSubdirectory(), files_count, &transaction);
|
||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
||||
}
|
||||
else {
|
||||
@@ -512,7 +515,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(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
}
|
||||
if (!stop_or_abort_requested()) {
|
||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
||||
@@ -524,7 +527,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
||||
void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
||||
|
||||
const QFileInfo path_info(path);
|
||||
|
||||
@@ -536,8 +539,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
return;
|
||||
}
|
||||
// Do not scan symlinked dirs that are already in collection
|
||||
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
|
||||
if (real_path.startsWith(dir.path)) {
|
||||
for (const CollectionDirectory &i : std::as_const(watched_dirs_)) {
|
||||
if (real_path.startsWith(i.path)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -578,7 +581,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
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(prev_subdir.path, prev_subdir, 0, t, true);
|
||||
ScanSubdirectory(dir, prev_subdir.path, prev_subdir, 0, t, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,11 +623,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
album_art[dir_part] << child_filepath;
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
|
||||
files_on_disk << child_filepath;
|
||||
}
|
||||
else {
|
||||
t->AddToProgress(1);
|
||||
files_on_disk << child_filepath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,7 +727,9 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
#endif
|
||||
|
||||
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
||||
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t);
|
||||
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t)) {
|
||||
files_on_disk.removeAll(file);
|
||||
}
|
||||
}
|
||||
else { // If CUE associated.
|
||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
||||
@@ -784,7 +786,9 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
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.
|
||||
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t);
|
||||
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t)) {
|
||||
files_on_disk.removeAll(file);
|
||||
}
|
||||
}
|
||||
else { // If CUE associated.
|
||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
||||
@@ -795,6 +799,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
|
||||
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
|
||||
if (songs.isEmpty()) {
|
||||
files_on_disk.removeAll(file);
|
||||
t->AddToProgress(1);
|
||||
continue;
|
||||
}
|
||||
@@ -805,7 +810,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
const QUrl art_automatic = ArtForSong(file, album_art);
|
||||
|
||||
for (Song song : songs) {
|
||||
song.set_directory_id(t->dir());
|
||||
song.set_directory_id(t->dir_id());
|
||||
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
|
||||
t->new_songs << song;
|
||||
}
|
||||
@@ -823,27 +828,26 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
}
|
||||
}
|
||||
|
||||
// Add this subdir to the new or touched list
|
||||
// Add, update or delete subdir
|
||||
CollectionSubdirectory updated_subdir;
|
||||
updated_subdir.directory_id = t->dir();
|
||||
updated_subdir.directory_id = t->dir_id();
|
||||
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
||||
updated_subdir.path = path;
|
||||
|
||||
if (subdir.directory_id == -1) {
|
||||
if (updated_subdir.mtime == 0 && updated_subdir.path != dir.path) {
|
||||
t->deleted_subdirs << updated_subdir;
|
||||
}
|
||||
else if (subdir.directory_id == -1) {
|
||||
t->new_subdirs << updated_subdir;
|
||||
}
|
||||
else {
|
||||
else if (subdir.mtime != updated_subdir.mtime) {
|
||||
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(my_new_subdir.path, my_new_subdir, 0, t, true);
|
||||
ScanSubdirectory(dir, my_new_subdir.path, my_new_subdir, 0, t, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -875,7 +879,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());
|
||||
new_cue_song.set_directory_id(t->dir_id());
|
||||
PerformEBUR128Analysis(new_cue_song);
|
||||
new_cue_song.set_fingerprint(fingerprint);
|
||||
|
||||
@@ -901,7 +905,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
const QString &fingerprint,
|
||||
const SongList &matching_songs,
|
||||
const QUrl &art_automatic,
|
||||
@@ -922,7 +926,7 @@ void 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());
|
||||
song_on_disk.set_directory_id(t->dir_id());
|
||||
song_on_disk.set_id(matching_song.id());
|
||||
PerformEBUR128Analysis(song_on_disk);
|
||||
song_on_disk.set_fingerprint(fingerprint);
|
||||
@@ -931,6 +935,8 @@ void 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 {
|
||||
@@ -1199,12 +1205,13 @@ void CollectionWatcher::DirectoryChanged(const QString &subdir) {
|
||||
|
||||
void CollectionWatcher::RescanPathsNow() {
|
||||
|
||||
const QList<int> dirs = rescan_queue_.keys();
|
||||
for (const int dir : dirs) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
|
||||
const QList<int> dir_ids = rescan_queue_.keys();
|
||||
for (const int dir_id : dir_ids) {
|
||||
|
||||
const QStringList paths = rescan_queue_.value(dir);
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanTransaction transaction(this, dir_id, false, false, mark_songs_unavailable_);
|
||||
|
||||
const QStringList paths = rescan_queue_.value(dir_id);
|
||||
|
||||
QMap<QString, quint64> subdir_files_count;
|
||||
for (const QString &path : paths) {
|
||||
@@ -1215,11 +1222,14 @@ 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;
|
||||
subdir.directory_id = dir_id;
|
||||
subdir.mtime = 0;
|
||||
subdir.path = path;
|
||||
ScanSubdirectory(path, subdir, subdir_files_count[path], &transaction);
|
||||
ScanSubdirectory(subdir_mapping_[path], path, subdir, subdir_files_count[path], &transaction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1344,11 +1354,13 @@ 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();
|
||||
|
||||
if (subdirs.isEmpty()) {
|
||||
qLog(Debug) << "Collection directory wasn't in subdir list.";
|
||||
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";
|
||||
CollectionSubdirectory subdir;
|
||||
subdir.path = dir.path;
|
||||
subdir.directory_id = dir.id;
|
||||
subdir.path = dir.path;
|
||||
subdir.mtime = 0;
|
||||
subdirs << subdir;
|
||||
}
|
||||
|
||||
@@ -1358,7 +1370,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(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1459,6 +1471,8 @@ 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_);
|
||||
@@ -1468,7 +1482,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(song_path, subdir, files_count, &transaction);
|
||||
ScanSubdirectory(dir, song_path, subdir, files_count, &transaction);
|
||||
scanned_paths << subdir.path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -85,6 +85,7 @@ 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();
|
||||
@@ -122,7 +123,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() const { return dir_; }
|
||||
int dir_id() const { return dir_id_; }
|
||||
bool is_incremental() const { return incremental_; }
|
||||
bool ignores_mtime() const { return ignores_mtime_; }
|
||||
|
||||
@@ -143,7 +144,7 @@ class CollectionWatcher : public QObject {
|
||||
quint64 progress_;
|
||||
quint64 progress_max_;
|
||||
|
||||
int dir_;
|
||||
int dir_id_;
|
||||
// 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.
|
||||
@@ -179,7 +180,7 @@ class CollectionWatcher : public QObject {
|
||||
void IncrementalScanNow();
|
||||
void FullScanNow();
|
||||
void RescanPathsNow();
|
||||
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
||||
void ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
||||
void RescanSongs(const SongList &songs);
|
||||
|
||||
private:
|
||||
@@ -202,7 +203,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.
|
||||
void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
|
||||
bool 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;
|
||||
|
||||
@@ -245,7 +245,6 @@ 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);
|
||||
@@ -280,6 +279,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
public Q_SLOTS:
|
||||
void CommandlineOptionsReceived(const QByteArray &string_options);
|
||||
void Raise();
|
||||
void Exit();
|
||||
|
||||
private:
|
||||
void SaveSettings();
|
||||
|
||||
@@ -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 || d->source_ == Source::Spotify; }
|
||||
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz; }
|
||||
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(); }
|
||||
|
||||
|
||||
175
src/core/unixsignalwatcher.cpp
Normal file
175
src/core/unixsignalwatcher.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
53
src/core/unixsignalwatcher.h
Normal file
53
src/core/unixsignalwatcher.h
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -75,6 +75,7 @@ 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);
|
||||
|
||||
12
src/main.cpp
12
src/main.cpp
@@ -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-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
|
||||
@@ -76,6 +76,10 @@
|
||||
|
||||
#include <kdsingleapplication.h>
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include "core/unixsignalwatcher.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_QTSPARKLE
|
||||
# include <qtsparkle-qt6/Updater>
|
||||
#endif // HAVE_QTSPARKLE
|
||||
@@ -365,6 +369,12 @@ 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
|
||||
|
||||
@@ -152,7 +152,6 @@ 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);
|
||||
|
||||
}
|
||||
|
||||
@@ -1227,7 +1227,7 @@ If there are no matches then it will use the largest image in the directory.</so
|
||||
</message>
|
||||
<message>
|
||||
<source>Don't show in various artists</source>
|
||||
<translation type="unfinished">Don't show in various artists</translation>
|
||||
<translation>Όχι εμφάνιση σε διάφορους καλλιτέχνες</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 type="unfinished">Set through album cover search (%1)</translation>
|
||||
<translation>Ορισμός μέσω αναζήτησης εξωφύλλου άλμπουμ (%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 type="unfinished">Saving CUE files is not supported.</translation>
|
||||
<translation>Η αποθήκευση αρχείων CUE δεν υποστηρίζεται.</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 type="unfinished">Different art across multiple songs.</translation>
|
||||
<translation>Διαφορετική τέχνη σε πολλαπλά τραγούδια.</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 type="unfinished">Toggle skip status</translation>
|
||||
<translation>Εναλλαγή κατάστασης παράκαμψης</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 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>
|
||||
<translation>Τρέχετε το Strawberry κάτω από Rosetta. Η χρήση του Strawberry κάτω από Rosetta δεν υποστηρίζεται και είναι γνωστό ότι παρουσιάζει προβλήματα. Θα πρέπει να λάβετε το Strawberry για τη σωστή αρχιτεκτονική CPU από %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 type="unfinished">Loudness Range</translation>
|
||||
<translation>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't create playlist</source>
|
||||
<translation type="unfinished">Couldn't create playlist</translation>
|
||||
<translation>Αδυναμία δημιουργίας λίστας αναπαραγωγής</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 type="unfinished">Warn me when closing a playlist tab</translation>
|
||||
<translation>Προειδοποίηση κατά το κλείσιμο μιας καρτέλας λίστας αναπαραγωγής</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>This option can be changed in the "Behavior" preferences</source>
|
||||
<translation type="unfinished">This option can be changed in the "Behavior" preferences</translation>
|
||||
<translation/>
|
||||
</message>
|
||||
<message>
|
||||
<source>Double-click here to favorite this playlist so it will be saved and remain accessible through the "Playlists" 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 type="unfinished">Loads files/URLs, replacing current playlist</translation>
|
||||
<translation>Φόρτωση αρχείων/URL, αντικατάσταση της τρέχουσας λίστας αναπαραγωγής</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Play the <n>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 type="unfinished">Destination file %1 exists, but not allowed to overwrite.</translation>
|
||||
<translation>Το αρχείο προορισμού %1 υπάρχει, αλλά δεν επιτρέπεται να αντικατασταθεί.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Destination file %1 exists, but not allowed to overwrite</source>
|
||||
<translation type="unfinished">Destination file %1 exists, but not allowed to overwrite</translation>
|
||||
<translation>Το αρχείο προορισμού %1 υπάρχει, αλλά δεν επιτρέπεται να αντικατασταθεί</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 type="unfinished">%1 songs in %2 different directories selected, are you sure you want to open them all?</translation>
|
||||
<translation>%1 τραγούδια σε διαφορετικούς καταλόγους %2 επιλέχθηκαν, είστε σίγουροι ότι θέλετε να τα ανοίξετε όλα;</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 type="unfinished">Receiving album cover for %1 album...</translation>
|
||||
<translation>Λήψη εξωφύλλου για το άλμπουμ %1...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Receiving album covers for %1 albums...</source>
|
||||
<translation type="unfinished">Receiving album covers for %1 albums...</translation>
|
||||
<translation>Λήψη εξώφυλλων για άλμπουμ %1...</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 track(s)</numerusform>
|
||||
<numerusform>%n κομμάτι(α)</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 type="unfinished">Move up</translation>
|
||||
<translation>Μετακίνηση πάνω</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Ctrl+Down</source>
|
||||
<translation type="unfinished">Ctrl+Down</translation>
|
||||
<translation>Ctrl+Down</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Remove</source>
|
||||
<translation type="unfinished">Remove</translation>
|
||||
<translation>Αφαίρεση</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 type="unfinished">Getting %1 channels</translation>
|
||||
<translation>Λήψη %1 καναλιών</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>RadioView</name>
|
||||
<message>
|
||||
<source>Append to current playlist</source>
|
||||
<translation type="unfinished">Append to current playlist</translation>
|
||||
<translation>Προσάρτηση στην τρέχουσα λίστα</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Replace current playlist</source>
|
||||
<translation type="unfinished">Replace current playlist</translation>
|
||||
<translation>Αντικατάσταση της τρέχουσας λίστας</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open in new playlist</source>
|
||||
<translation type="unfinished">Open in new playlist</translation>
|
||||
<translation>Άνοιγμα σε νέα λίστα</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open homepage</source>
|
||||
<translation type="unfinished">Open homepage</translation>
|
||||
<translation>Άνοιγμα αρχικής σελίδας</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 type="unfinished">Select directory for saving playlists</translation>
|
||||
<translation>Επιλέξτε φάκελο για τις λίστες αναπαραγωγής</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 type="unfinished">Show love button</translation>
|
||||
<translation>Εμφάνιση πλήκτρου αγάπης</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Submit scrobbles every</source>
|
||||
<translation type="unfinished">Submit scrobbles every</translation>
|
||||
<translation>Υποβολή scrobbles κάθε</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 type="unfinished">Offline mode (Only cache scrobbles)</translation>
|
||||
<translation>Λειτουργία εκτός σύνδεσης (Μόνο 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 type="unfinished">Open URL in web browser?</translation>
|
||||
<translation>Άνοιγμα διεύθυνσης URL στο πρόγραμμα περιήγησης;</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press "Save" to copy the URL to clipboard and manually open it in a web browser.</source>
|
||||
<translation type="unfinished">Press "Save" to copy the URL to clipboard and manually open it in a web browser.</translation>
|
||||
<translation>Πατήστε "Save" για να αντιγράψετε το URL στο πρόχειρο και να το ανοίξετε χειροκίνητα σε ένα πρόγραμμα περιήγησης.</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 type="unfinished">Invalid reply from web browser. Missing token.</translation>
|
||||
<translation>Μη έγκυρη απάντηση από το πρόγραμμα περιήγησης. Λείπει token.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Received invalid reply from web browser. Try another browser.</source>
|
||||
<translation type="unfinished">Received invalid reply from web browser. Try another browser.</translation>
|
||||
<translation>Λήφθηκε μη έγκυρη απάντηση από το πρόγραμμα περιήγησης. Δοκιμάστε ένα άλλο πρόγραμμα περιήγησης.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scrobbler %1 is not authenticated!</source>
|
||||
<translation type="unfinished">Scrobbler %1 is not authenticated!</translation>
|
||||
<translation>Το Scrobbler %1 δεν είναι πιστοποιημένο!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scrobbler %1 error: %2</source>
|
||||
<translation type="unfinished">Scrobbler %1 error: %2</translation>
|
||||
<translation>Scrobbler %1 σφάλμα: %2</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -5873,7 +5873,7 @@ Are you sure you want to continue?</source>
|
||||
</message>
|
||||
<message>
|
||||
<source>Streaming</source>
|
||||
<translation type="unfinished">Streaming</translation>
|
||||
<translation>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 type="unfinished">Choose a name for your smart playlist</translation>
|
||||
<translation>Επιλέξτε ένα όνομα για την έξυπνη λίστα αναπαραγωγής σας</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SmartPlaylistWizardFinishPage</name>
|
||||
<message>
|
||||
<source>Form</source>
|
||||
<translation type="unfinished">Form</translation>
|
||||
<translation>Φόρμα</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 type="unfinished">Use dynamic mode</translation>
|
||||
<translation>Χρήση δυναμικής λειτουργίας</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>In dynamic mode new tracks will be chosen and added to the playlist every time a song finishes.</source>
|
||||
|
||||
@@ -119,11 +119,11 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to open cover file %1 for reading: %2</source>
|
||||
<translation type="unfinished">Failed to open cover file %1 for reading: %2</translation>
|
||||
<translation>No se pudo abrir el archivo de portada %1 para lectura: %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cover file %1 is empty.</source>
|
||||
<translation type="unfinished">Cover file %1 is empty.</translation>
|
||||
<translation>El archivo de portada %1 está vacío.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>unknown</source>
|
||||
@@ -135,27 +135,27 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to open cover file %1 for writing: %2</source>
|
||||
<translation type="unfinished">Failed to open cover file %1 for writing: %2</translation>
|
||||
<translation>No se pudo abrir el archivo de portada %1 para escribir: %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed writing cover to file %1: %2</source>
|
||||
<translation type="unfinished">Failed writing cover to file %1: %2</translation>
|
||||
<translation>Error al escribir la portada en el archivo %1: %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed writing cover to file %1.</source>
|
||||
<translation type="unfinished">Failed writing cover to file %1.</translation>
|
||||
<translation>Error al escribir la portada en el archivo %1.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to delete cover file %1: %2</source>
|
||||
<translation type="unfinished">Failed to delete cover file %1: %2</translation>
|
||||
<translation>No se pudo eliminar el archivo de portada %1: %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to write cover to file %1: %2</source>
|
||||
<translation type="unfinished">Failed to write cover to file %1: %2</translation>
|
||||
<translation>No se pudo escribir la portada en el archivo %1: %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Could not save cover to file %1.</source>
|
||||
<translation type="unfinished">Could not save cover to file %1.</translation>
|
||||
<translation>No se pudo guardar la portada en el archivo %1.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -273,7 +273,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Could not save cover to file %1.</source>
|
||||
<translation type="unfinished">Could not save cover to file %1.</translation>
|
||||
<translation>No se pudo guardar la portada en el archivo %1.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -339,7 +339,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Turbine</source>
|
||||
<translation type="unfinished">Turbine</translation>
|
||||
<translation>Turbina</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sonogram</source>
|
||||
@@ -549,7 +549,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>p&lughw</source>
|
||||
<translation type="unfinished">p&lughw</translation>
|
||||
<translation>enchufe</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>pcm</source>
|
||||
@@ -569,7 +569,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Upmix / downmix to</source>
|
||||
<translation type="unfinished">Upmix / downmix to</translation>
|
||||
<translation>Mezcla ascendente/descendente a</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>channels</source>
|
||||
@@ -577,15 +577,15 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Improve headphone listening of stereo audio records (bs2b)</source>
|
||||
<translation type="unfinished">Improve headphone listening of stereo audio records (bs2b)</translation>
|
||||
<translation>Mejorar la escucha de grabaciones de audio estéreo con auriculares (B2B)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable HTTP/2 for streaming</source>
|
||||
<translation type="unfinished">Enable HTTP/2 for streaming</translation>
|
||||
<translation>Habilitar HTTP/2 para transmisión</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Use strict SSL mode</source>
|
||||
<translation type="unfinished">Use strict SSL mode</translation>
|
||||
<translation>Utilice el modo SSL estricto</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Buffer</source>
|
||||
@@ -649,19 +649,19 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Fallback-gain</source>
|
||||
<translation type="unfinished">Fallback-gain</translation>
|
||||
<translation>Ganancia de respaldo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>EBU R 128 Loudness Normalization</source>
|
||||
<translation type="unfinished">EBU R 128 Loudness Normalization</translation>
|
||||
<translation>Normalización de sonoridad EBU R 128</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Perform track loudness normalization</source>
|
||||
<translation type="unfinished">Perform track loudness normalization</translation>
|
||||
<translation>Realizar la normalización de la sonoridad de la pista</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Target Level</source>
|
||||
<translation type="unfinished">Target Level</translation>
|
||||
<translation>Nivel objetivo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fading</source>
|
||||
@@ -712,7 +712,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Show song progress on taskbar</source>
|
||||
<translation type="unfinished">Show song progress on taskbar</translation>
|
||||
<translation>Mostrar el progreso de la canción en la barra de tareas</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 type="unfinished">Unable to execute collection SQL query: %1</translation>
|
||||
<translation>No se puede ejecutar la consulta SQL de recopilación: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed SQL query: %1</source>
|
||||
<translation type="unfinished">Failed SQL query: %1</translation>
|
||||
<translation>Consulta SQL fallida: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Updating %1 database.</source>
|
||||
<translation type="unfinished">Updating %1 database.</translation>
|
||||
<translation>Actualizando la base de datos %1.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -873,7 +873,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>MenuPopupToolButton</source>
|
||||
<translation type="unfinished">MenuPopupToolButton</translation>
|
||||
<translation>Botón de herramienta emergente de menú</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Entire collection</source>
|
||||
@@ -992,7 +992,7 @@
|
||||
<name>CollectionLibrary</name>
|
||||
<message>
|
||||
<source>Saving playcounts and ratings</source>
|
||||
<translation type="unfinished">Saving playcounts and ratings</translation>
|
||||
<translation>Guardar recuentos de reproducciones y calificaciones</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 type="unfinished">Perform song EBU R 128 analysis (required for EBU R 128 loudness normalization)</translation>
|
||||
<translation>Realizar el análisis de la canción EBU R 128 (necesario para la normalización de la sonoridad EBU R 128)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Expire unavailable songs after</source>
|
||||
<translation type="unfinished">Expire unavailable songs after</translation>
|
||||
<translation>Las canciones no disponibles expirarán después de</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 type="unfinished">Use various artists for compilation albums</translation>
|
||||
<translation>Utilice varios artistas para álbumes recopilatorios</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Skip leading articles ("the", "a", "an") when sorting artist names</source>
|
||||
<translation type="unfinished">Skip leading articles ("the", "a", "an") when sorting artist names</translation>
|
||||
<translation>Omitir los artículos iniciales ("él", "un", "una") al ordenar los nombres de los artistas</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 type="unfinished">Song playcounts and ratings</translation>
|
||||
<translation>Número de reproducciones y calificaciones de canciones</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save playcounts to song tags when possible</source>
|
||||
<translation type="unfinished">Save playcounts to song tags when possible</translation>
|
||||
<translation>Guarde los recuentos de reproducciones en las etiquetas de las canciones cuando sea posible</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save ratings to song tags when possible</source>
|
||||
<translation type="unfinished">Save ratings to song tags when possible</translation>
|
||||
<translation>Guarde las calificaciones en las etiquetas de las canciones cuando sea posible</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Overwrite database playcount when songs are re-read from disk</source>
|
||||
<translation type="unfinished">Overwrite database playcount when songs are re-read from disk</translation>
|
||||
<translation>Sobrescribir el recuento de reproducciones de la base de datos cuando se vuelven a leer las canciones desde el disco</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Overwrite database rating when songs are re-read from disk</source>
|
||||
<translation type="unfinished">Overwrite database rating when songs are re-read from disk</translation>
|
||||
<translation>Sobrescribir la clasificación de la base de datos cuando se vuelven a leer las canciones desde el disco</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save playcounts and ratings to files now</source>
|
||||
<translation type="unfinished">Save playcounts and ratings to files now</translation>
|
||||
<translation>Guarda recuentos de reproducciones y calificaciones en archivos ahora</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 type="unfinished">Write all playcounts and ratings to files</translation>
|
||||
<translation>Escribe todos los recuentos de reproducción y calificaciones en archivos</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 type="unfinished">Error</translation>
|
||||
<translation>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 type="unfinished">EBU R 128 Integrated Loudness</translation>
|
||||
<translation>EBU R 128 Sonoridad integrada</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>EBU R 128 Loudness Range</source>
|
||||
<translation type="unfinished">EBU R 128 Loudness Range</translation>
|
||||
<translation/>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show album cover</source>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>About Strawberry</source>
|
||||
<translation>Strawberry teave</translation>
|
||||
<translation>Rakenduse teave: Strawberry</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Version %1</source>
|
||||
@@ -45,7 +45,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Contributors</source>
|
||||
<translation>Toetajad</translation>
|
||||
<translation>Kaasautorid</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Clementine authors</source>
|
||||
@@ -53,7 +53,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Clementine contributors</source>
|
||||
<translation>Clementine'i toetajad</translation>
|
||||
<translation>Clementine'i kaasautorid</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Thanks to</source>
|
||||
@@ -233,7 +233,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Really cancel?</source>
|
||||
<translation>Kas tühistada?</translation>
|
||||
<translation>Kas tõesti katkestame?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Closing this window will stop searching for album covers.</source>
|
||||
|
||||
@@ -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>Нормализация громкости</translation>
|
||||
<translation>Нормализация громкости (Replay Gain)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Use Replay Gain metadata if it is available</source>
|
||||
<translation>Использовать метаданные нормализации по возможности</translation>
|
||||
<translation>Использовать метаданные нормализации (Replay Gain) по возможности</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 "Previous" 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>
|
||||
|
||||
Reference in New Issue
Block a user