Merge remote-tracking branch 'upstream/master' into qt5

This commit is contained in:
Chocobozzz 2017-06-05 21:28:05 +02:00
commit fc517ce7a5
129 changed files with 12991 additions and 11826 deletions

View File

@ -1,4 +1,4 @@
add_custom_target(format-diff
COMMAND python ${CMAKE_SOURCE_DIR}/dist/format.py)
COMMAND python2 ${CMAKE_SOURCE_DIR}/dist/format.py)
add_custom_target(format
COMMAND python ${CMAKE_SOURCE_DIR}/dist/format.py -i)
COMMAND python2 ${CMAKE_SOURCE_DIR}/dist/format.py -i)

2
dist/codesign.py vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
# Emulates the behaviour of codesign --deep which is missing on OS X < 10.9
import os

24
dist/copyright.py vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
from subprocess import *
from sys import *
@ -6,7 +6,7 @@ from os import rename, remove
from datetime import *
def pretty_years(s):
l = list(s)
l.sort()
@ -23,7 +23,7 @@ def pretty_years(s):
if x == prev + 1:
prev = x
continue
if prev == start:
r.append("%i" % prev)
else:
@ -41,10 +41,10 @@ def pretty_years(s):
return ", ".join(r)
def order_by_year(a, b):
la = list(a[2])
la.sort()
lb = list(b[2])
lb.sort()
@ -60,9 +60,9 @@ def gen_copyrights(f):
commits = []
data = {}
copyrights = []
for ln in Popen(["git", "blame", "--incremental", f], stdout=PIPE).stdout:
if ln.startswith("filename "):
if len(data) > 0:
commits.append(data)
@ -110,7 +110,7 @@ def gen_copyrights(f):
if a[1] == b[1]:
a[2].update(b[2])
if by_author.has_key(an) and by_author.has_key(bn):
if by_author.has_key(an) and by_author.has_key(bn):
del by_author[bn]
copyright = list(by_author.itervalues())
@ -129,7 +129,7 @@ def change_file(filename):
content=fi.readlines()
copyrights=gen_copyrights(filename)
if -1 == content[0].find("/* This file is part of Clementine."):
print("File {} have no Clementine copyright info".format(filename))
return 0
@ -141,16 +141,16 @@ def change_file(filename):
if not extended:
out.extend(copyrights)
extended = 1
if not ends:
continue
else:
out.append(i)
with open(filename+'_tmp', "w") as fi:
fi.writelines(out)
rename(filename+'_tmp', filename)
if __name__ == "__main__":
for files in argv[1:]:

4
dist/cpplint.py vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
#
# Copyright (c) 2009 Google Inc. All rights reserved.
#
@ -4646,7 +4646,7 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension,
# Make Windows paths like Unix.
fullname = os.path.abspath(filename).replace('\\', '/')
# Perform other checks now that we are sure that this is not an include line
CheckCasts(filename, clean_lines, linenum, error)
CheckGlobalStatic(filename, clean_lines, linenum, error)

2
dist/format.py vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python2
import argparse
import difflib
import os

2
dist/macdeploy.py vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
# This file is part of Clementine.
#

View File

@ -1,3 +1,4 @@
#!/usr/bin/python2
import codecs
import glob
import logging

View File

@ -1,3 +1,4 @@
#!/usr/bin/python2
import rpm
import subprocess

View File

@ -1,3 +1,23 @@
/* This file is part of Clementine.
Copyright 2017, David Sansome <me@davidsansome.com>
Copyright 2017, Andreas Muttscheller <asfa194@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL, so
// 3rd party applications or libraries can use another license besides GPL.
package pb.remote;
// The supported message types

View File

@ -1197,7 +1197,6 @@ qt5_wrap_cpp(MOC ${HEADERS})
qt5_wrap_ui(UIC ${UI})
qt5_add_resources(QRC ${RESOURCES})
add_pot(POT
${CMAKE_CURRENT_SOURCE_DIR}/translations/header
${CMAKE_CURRENT_SOURCE_DIR}/translations/translations.pot

View File

@ -62,14 +62,13 @@ static const int sBarkBandCount = arraysize(sBarkBands);
Analyzer::Base::Base(QWidget* parent, uint scopeSize)
: QWidget(parent),
timeout_(40) // msec
,
timeout_(40), // msec
fht_(new FHT(scopeSize)),
engine_(nullptr),
lastScope_(512),
new_frame_(false),
is_playing_(false),
barkband_table_(QList<uint>()),
barkband_table_(),
prev_color_index_(0),
bands_(0),
psychedelic_enabled_(false) {}
@ -86,15 +85,17 @@ void Analyzer::Base::transform(Scope& scope) {
// values
// scope.resize( fht_->size() );
float* front = static_cast<float*>(&scope.front());
QVector<float> aux(fht_->size());
if (aux.size() >= scope.size()) {
qCopy(scope.begin(), scope.end(), aux.begin());
} else {
qCopy(scope.begin(), scope.begin() + aux.size(), aux.begin());
}
float* f = new float[fht_->size()];
fht_->copy(&f[0], front);
fht_->logSpectrum(front, &f[0]);
fht_->scale(front, 1.0 / 20);
fht_->logSpectrum(scope.data(), aux.data());
fht_->scale(scope.data(), 1.0 / 20);
scope.resize(fht_->size() / 2); // second half of values are rubbish
delete[] f;
}
void Analyzer::Base::paintEvent(QPaintEvent* e) {
@ -202,7 +203,6 @@ void Analyzer::Base::updateBandSize(const int scopeSize) {
bands_ = scopeSize;
barkband_table_.clear();
barkband_table_.reserve(bands_ + 1);
int barkband = 0;
for (int i = 0; i < bands_ + 1; ++i) {
@ -218,7 +218,7 @@ void Analyzer::Base::updateBandSize(const int scopeSize) {
QColor Analyzer::Base::getPsychedelicColor(const Scope& scope,
const int ampFactor,
const int bias) {
if (scope.size() > barkband_table_.length()) {
if (scope.size() > barkband_table_.size()) {
return palette().color(QPalette::Highlight);
}
@ -232,15 +232,12 @@ QColor Analyzer::Base::getPsychedelicColor(const Scope& scope,
// Now divide the bark bands into thirds and compute their total amplitudes.
double rgb[3]{};
for (int i = 0; i < sBarkBandCount - 1; ++i) {
rgb[(i * 3) / sBarkBandCount] += bands[i] * bands[i];
rgb[(i * 3) / sBarkBandCount] += pow(bands[i], 2);
}
for (int i = 0; i < 3; ++i) {
// bias colours for a threshold around normally amplified audio
rgb[i] = (int)((sqrt(rgb[i]) * ampFactor) + bias);
if (rgb[i] > 255) {
rgb[i] = 255;
}
rgb[i] = qMin(255, (int)((sqrt(rgb[i]) * ampFactor) + bias));
}
return QColor::fromRgb(rgb[0], rgb[1], rgb[2]);

View File

@ -109,7 +109,7 @@ class Base : public QWidget {
bool new_frame_;
bool is_playing_;
QList<uint> barkband_table_;
QVector<uint> barkband_table_;
double prev_colors_[10][3];
int prev_color_index_;
int bands_;

View File

@ -100,8 +100,11 @@ void BarAnalyzer::colorChanged() {
rgb = palette().color(QPalette::Highlight);
}
for (int x = 0, r = rgb.red(), g = rgb.green(), b = rgb.blue(), r2 = 255 - r;
x < height(); ++x) {
for (int x = 0; x < height(); ++x) {
int r = rgb.red();
int g = rgb.green();
int b = rgb.blue();
int r2 = 255 - r;
for (int y = x; y > 0; --y) {
const double fraction = static_cast<double>(y) / height();
@ -121,7 +124,7 @@ void BarAnalyzer::psychedelicModeChanged(bool enabled) {
}
void BarAnalyzer::analyze(QPainter& p, const Scope& s, bool new_frame) {
if (!new_frame || engine_->state() == Engine::Paused) {
if (!new_frame || engine_->state() == Engine::Paused) {
p.drawPixmap(0, 0, canvas_);
return;
}

View File

@ -127,10 +127,8 @@ void BlockAnalyzer::framerateChanged() { // virtual
void BlockAnalyzer::transform(Analyzer::Scope& s) {
for (uint x = 0; x < s.size(); ++x) s[x] *= 2;
float* front = static_cast<float*>(&s.front());
fht_->spectrum(front);
fht_->scale(front, 1.0 / 20);
fht_->spectrum(s.data());
fht_->scale(s.data(), 1.0 / 20);
// the second half is pretty dull, so only show it if the user has a large
// analyzer
@ -400,6 +398,10 @@ void BlockAnalyzer::paletteChange(const QPalette&) {
}
void BlockAnalyzer::drawBackground() {
if (background_.isNull()) {
return;
}
const QColor bg = palette().color(QPalette::Background);
const QColor bgdark = bg.dark(112);

View File

@ -68,14 +68,13 @@ class BlockAnalyzer : public Analyzer::Base {
QPixmap topBarPixmap_;
QPixmap background_;
QPixmap canvas_;
Analyzer::Scope scope_; // so we don't create a vector every frame
std::vector<float> store_; // current bar kHeights
std::vector<float> yscale_;
Analyzer::Scope scope_; // so we don't create a vector every frame
QVector<float> store_; // current bar kHeights
QVector<float> yscale_;
// FIXME why can't I namespace these? c++ issue?
std::vector<QPixmap> fade_bars_;
std::vector<uint> fade_pos_;
std::vector<int> fade_intensity_;
QVector<QPixmap> fade_bars_;
QVector<uint> fade_pos_;
QVector<int> fade_intensity_;
float step_; // rows to fall per frame
};

View File

@ -93,10 +93,8 @@ void BoomAnalyzer::resizeEvent(QResizeEvent* e) {
}
void BoomAnalyzer::transform(Scope& s) {
float* front = static_cast<float*>(&s.front());
fht_->spectrum(front);
fht_->scale(front, 1.0 / 50);
fht_->spectrum(s.data());
fht_->scale(s.data(), 1.0 / 50);
s.resize(scope_.size() <= kMaxBandCount / 2 ? kMaxBandCount / 2
: scope_.size());

View File

@ -20,52 +20,40 @@
/* Original Author: Melchior FRANZ <mfranz@kde.org> 2004
*/
#include <math.h>
#include <string.h>
#include <cmath>
#include "fht.h"
FHT::FHT(int n) : buf_(0), tab_(0), log_(0) {
if (n < 3) {
num_ = 0;
exp2_ = -1;
return;
}
exp2_ = n;
num_ = 1 << n;
FHT::FHT(int n) : num_((n < 3) ? 0 : 1 << n), exp2_((n < 3) ? -1 : n) {
if (n > 3) {
buf_ = new float[num_];
tab_ = new float[num_ * 2];
buf_vector_.resize(num_);
tab_vector_.resize(num_ * 2);
makeCasTable();
}
}
FHT::~FHT() {
delete[] buf_;
delete[] tab_;
delete[] log_;
}
FHT::~FHT() {}
int FHT::sizeExp() const { return exp2_; }
int FHT::size() const { return num_; }
float* FHT::buf_() { return buf_vector_.data(); }
float* FHT::tab_() { return tab_vector_.data(); }
int* FHT::log_() { return log_vector_.data(); }
void FHT::makeCasTable(void) {
float d, *costab, *sintab;
int ul, ndiv2 = num_ / 2;
float* costab = tab_();
float* sintab = tab_() + num_ / 2 + 1;
for (costab = tab_, sintab = tab_ + num_ / 2 + 1, ul = 0; ul < num_; ul++) {
d = M_PI * ul / ndiv2;
for (int ul = 0; ul < num_; ul++) {
float d = M_PI * ul / (num_ / 2);
*costab = *sintab = cos(d);
costab += 2, sintab += 2;
if (sintab > tab_ + num_ * 2) sintab = tab_ + 1;
costab += 2;
sintab += 2;
if (sintab > tab_() + num_ * 2) sintab = tab_() + 1;
}
}
float* FHT::copy(float* d, float* s) {
return static_cast<float*>(memcpy(d, s, num_ * sizeof(float)));
}
float* FHT::clear(float* d) {
return static_cast<float*>(memset(d, 0, num_ * sizeof(float)));
}
void FHT::scale(float* p, float d) {
for (int i = 0; i < (num_ / 2); i++) *p++ *= d;
}
@ -76,17 +64,17 @@ void FHT::ewma(float* d, float* s, float w) {
void FHT::logSpectrum(float* out, float* p) {
int n = num_ / 2, i, j, k, *r;
if (!log_) {
log_ = new int[n];
if (log_vector_.size() < n) {
log_vector_.resize(n);
float f = n / log10(static_cast<double>(n));
for (i = 0, r = log_; i < n; i++, r++) {
for (i = 0, r = log_(); i < n; i++, r++) {
j = static_cast<int>(rint(log10(i + 1.0) * f));
*r = j >= n ? n - 1 : j;
}
}
semiLogSpectrum(p);
*out++ = *p = *p / 100;
for (k = i = 1, r = log_; i < n; i++) {
for (k = i = 1, r = log_(); i < n; i++) {
j = *r++;
if (i == j) {
*out++ = p[i];
@ -99,10 +87,9 @@ void FHT::logSpectrum(float* out, float* p) {
}
void FHT::semiLogSpectrum(float* p) {
float e;
power2(p);
for (int i = 0; i < (num_ / 2); i++, p++) {
e = 10.0 * log10(sqrt(*p * .5));
float e = 10.0 * log10(sqrt(*p / 2));
*p = e < 0 ? 0 : e;
}
}
@ -110,23 +97,26 @@ void FHT::semiLogSpectrum(float* p) {
void FHT::spectrum(float* p) {
power2(p);
for (int i = 0; i < (num_ / 2); i++, p++)
*p = static_cast<float>(sqrt(*p * .5));
*p = static_cast<float>(sqrt(*p / 2));
}
void FHT::power(float* p) {
power2(p);
for (int i = 0; i < (num_ / 2); i++) *p++ *= .5;
for (int i = 0; i < (num_ / 2); i++) *p++ /= 2;
}
void FHT::power2(float* p) {
int i;
float* q;
_transform(p, num_, 0);
*p = (*p * *p), *p += *p, p++;
*p = static_cast<float>(2 * pow(*p, 2));
p++;
for (i = 1, q = p + num_ - 2; i < (num_ / 2); i++, --q)
*p = (*p * *p) + (*q * *q), p++;
float* q = p + num_ - 2;
for (int i = 1; i < (num_ / 2); i++) {
*p = static_cast<float>(pow(*p, 2) + pow(*q, 2));
p++;
q--;
}
}
void FHT::transform(float* p) {
@ -172,19 +162,19 @@ void FHT::_transform(float* p, int n, int k) {
int i, j, ndiv2 = n / 2;
float a, *t1, *t2, *t3, *t4, *ptab, *pp;
for (i = 0, t1 = buf_, t2 = buf_ + ndiv2, pp = &p[k]; i < ndiv2; i++)
for (i = 0, t1 = buf_(), t2 = buf_() + ndiv2, pp = &p[k]; i < ndiv2; i++)
*t1++ = *pp++, *t2++ = *pp++;
memcpy(p + k, buf_, sizeof(float) * n);
std::copy(buf_(), buf_() + n, p + k);
_transform(p, ndiv2, k);
_transform(p, ndiv2, k + ndiv2);
j = num_ / ndiv2 - 1;
t1 = buf_;
t1 = buf_();
t2 = t1 + ndiv2;
t3 = p + k + ndiv2;
ptab = tab_;
ptab = tab_();
pp = p + k;
a = *ptab++ * *t3++;
@ -201,5 +191,6 @@ void FHT::_transform(float* p, int n, int k) {
*t1++ = *pp + a;
*t2++ = *pp++ - a;
}
memcpy(p + k, buf_, sizeof(float) * n);
std::copy(buf_(), buf_() + n, p + k);
}

View File

@ -23,6 +23,8 @@
#ifndef ANALYZERS_FHT_H_
#define ANALYZERS_FHT_H_
#include <QVector>
/**
* Implementation of the Hartley Transform after Bracewell's discrete
* algorithm. The algorithm is subject to US patent No. 4,646,256 (1987)
@ -32,11 +34,16 @@
* [1] Computer in Physics, Vol. 9, No. 4, Jul/Aug 1995 pp 373-379
*/
class FHT {
int exp2_;
int num_;
float* buf_;
float* tab_;
int* log_;
const int num_;
const int exp2_;
QVector<float> buf_vector_;
QVector<float> tab_vector_;
QVector<int> log_vector_;
float* buf_();
float* tab_();
int* log_();
/**
* Create a table of "cas" (cosine and sine) values.
@ -56,13 +63,11 @@ class FHT {
* should be at least 3. Values of more than 3 need a trigonometry table.
* @see makeCasTable()
*/
explicit FHT(int);
FHT(int);
~FHT();
inline int sizeExp() const { return exp2_; }
inline int size() const { return num_; }
float* copy(float*, float*);
float* clear(float*);
int sizeExp() const;
int size() const;
void scale(float*, float);
/**

View File

@ -6,7 +6,7 @@
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2015, Arun Narayanankutty <n.arun.lifescience@gmail.com>
Clementine 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
@ -26,19 +26,19 @@
#include <cmath>
#include <QTimerEvent>
#include <QBrush>
#include <QBrush>
#include "core/arraysize.h"
#include "core/logging.h"
using Analyzer::Scope;
const int Rainbow::RainbowAnalyzer::kHeight[] = { 21, 33 };
const int Rainbow::RainbowAnalyzer::kWidth[] = { 34, 53 };
const int Rainbow::RainbowAnalyzer::kFrameCount[] = { 6, 16 };
const int Rainbow::RainbowAnalyzer::kRainbowHeight[] = { 21, 16 };
const int Rainbow::RainbowAnalyzer::kRainbowOverlap[] = { 13, 15 };
const int Rainbow::RainbowAnalyzer::kSleepingHeight[] = { 24, 33 };
const int Rainbow::RainbowAnalyzer::kHeight[] = {21, 33};
const int Rainbow::RainbowAnalyzer::kWidth[] = {34, 53};
const int Rainbow::RainbowAnalyzer::kFrameCount[] = {6, 16};
const int Rainbow::RainbowAnalyzer::kRainbowHeight[] = {21, 16};
const int Rainbow::RainbowAnalyzer::kRainbowOverlap[] = {13, 15};
const int Rainbow::RainbowAnalyzer::kSleepingHeight[] = {24, 33};
const char* Rainbow::NyanCatAnalyzer::kName = "Nyanalyzer Cat";
const char* Rainbow::RainbowDashAnalyzer::kName = "Rainbow Dash";
@ -46,7 +46,8 @@ const float Rainbow::RainbowAnalyzer::kPixelScale = 0.02f;
Rainbow::RainbowAnalyzer::RainbowType Rainbow::RainbowAnalyzer::rainbowtype;
Rainbow::RainbowAnalyzer::RainbowAnalyzer(const RainbowType& rbtype, QWidget* parent)
Rainbow::RainbowAnalyzer::RainbowAnalyzer(const RainbowType& rbtype,
QWidget* parent)
: Analyzer::Base(parent, 9),
timer_id_(startTimer(kFrameIntervalMs)),
frame_(0),
@ -62,7 +63,7 @@ Rainbow::RainbowAnalyzer::RainbowAnalyzer(const RainbowType& rbtype, QWidget* pa
for (int i = 0; i < kRainbowBands; ++i) {
colors_[i] = QPen(QColor::fromHsv(i * 255 / kRainbowBands, 255, 255),
kRainbowHeight[rainbowtype] / kRainbowBands,
kRainbowHeight[rainbowtype] / kRainbowBands,
Qt::SolidLine, Qt::FlatCap, Qt::RoundJoin);
// pow constants computed so that
@ -72,7 +73,7 @@ Rainbow::RainbowAnalyzer::RainbowAnalyzer(const RainbowType& rbtype, QWidget* pa
}
}
void Rainbow::RainbowAnalyzer::transform(Scope& s) { fht_->spectrum(&s.front()); }
void Rainbow::RainbowAnalyzer::transform(Scope& s) { fht_->spectrum(s.data()); }
void Rainbow::RainbowAnalyzer::timerEvent(QTimerEvent* e) {
if (e->timerId() == timer_id_) {
@ -88,15 +89,15 @@ void Rainbow::RainbowAnalyzer::resizeEvent(QResizeEvent* e) {
buffer_[0] = QPixmap();
buffer_[1] = QPixmap();
available_rainbow_width_ = width() - kWidth[rainbowtype]
+ kRainbowOverlap[rainbowtype];
available_rainbow_width_ =
width() - kWidth[rainbowtype] + kRainbowOverlap[rainbowtype];
px_per_frame_ =
static_cast<float>(available_rainbow_width_) / (kHistorySize - 1) + 1;
x_offset_ = px_per_frame_ * (kHistorySize - 1) - available_rainbow_width_;
}
void Rainbow::RainbowAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
bool new_frame) {
bool new_frame) {
// Discard the second half of the transform
const int scope_size = s.size() / 2;
@ -129,15 +130,13 @@ void Rainbow::RainbowAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
QPointF* dest = polyline;
float* source = history_;
const float top_of =
static_cast<float>(height()) / 2 - static_cast<float>(
kRainbowHeight[rainbowtype]) / 2;
const float top_of = static_cast<float>(height()) / 2 -
static_cast<float>(kRainbowHeight[rainbowtype]) / 2;
for (int band = 0; band < kRainbowBands; ++band) {
// Calculate the Y position of this band.
const float y =
static_cast<float>(kRainbowHeight[rainbowtype]) / (
kRainbowBands + 1) * (band + 0.5) +
top_of;
const float y = static_cast<float>(kRainbowHeight[rainbowtype]) /
(kRainbowBands + 1) * (band + 0.5) +
top_of;
// Add each point in the line.
for (int x = 0; x < kHistorySize; ++x) {
@ -178,7 +177,7 @@ void Rainbow::RainbowAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
x_offset_ + available_rainbow_width_ - px_per_frame_, 0);
buffer_painter.fillRect(
x_offset_ + available_rainbow_width_ - px_per_frame_, 0,
kWidth[rainbowtype] - kRainbowOverlap[rainbowtype] + px_per_frame_,
kWidth[rainbowtype] - kRainbowOverlap[rainbowtype] + px_per_frame_,
height(), background_brush_);
for (int band = kRainbowBands - 1; band >= 0; --band) {
@ -196,18 +195,16 @@ void Rainbow::RainbowAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
// Nyan nyan nyan nyan dash dash dash dash.
if (!is_playing_) {
// Ssshhh!
p.drawPixmap(SleepingDestRect(rainbowtype), cat_dash_[rainbowtype],
p.drawPixmap(SleepingDestRect(rainbowtype), cat_dash_[rainbowtype],
SleepingSourceRect(rainbowtype));
} else {
p.drawPixmap(DestRect(rainbowtype), cat_dash_[rainbowtype],
p.drawPixmap(DestRect(rainbowtype), cat_dash_[rainbowtype],
SourceRect(rainbowtype));
}
}
Rainbow::NyanCatAnalyzer::NyanCatAnalyzer(QWidget* parent)
:RainbowAnalyzer(Rainbow::RainbowAnalyzer::Nyancat, parent) {
}
: RainbowAnalyzer(Rainbow::RainbowAnalyzer::Nyancat, parent) {}
Rainbow::RainbowDashAnalyzer::RainbowDashAnalyzer(QWidget* parent)
:RainbowAnalyzer(Rainbow::RainbowAnalyzer::Dash, parent) {
}
: RainbowAnalyzer(Rainbow::RainbowAnalyzer::Dash, parent) {}

View File

@ -114,9 +114,8 @@ void Sonogram::analyze(QPainter& p, const Scope& s, bool new_frame) {
}
void Sonogram::transform(Scope& scope) {
float* front = static_cast<float*>(&scope.front());
fht_->power2(front);
fht_->scale(front, 1.0 / 256);
fht_->power2(scope.data());
fht_->scale(scope.data(), 1.0 / 256);
scope.resize(fht_->size() / 2);
}

View File

@ -40,7 +40,6 @@ void TurbineAnalyzer::analyze(QPainter& p, const Scope& scope, bool new_frame) {
return;
}
float h;
const uint hd2 = height() / 2;
const uint kMaxHeight = hd2 - 1;
@ -55,13 +54,10 @@ void TurbineAnalyzer::analyze(QPainter& p, const Scope& scope, bool new_frame) {
}
for (uint i = 0, x = 0, y; i < bands_; ++i, x += kColumnWidth + 1) {
h = log10(scope_[i] * 256.0) * F_ * 0.5;
if (h > kMaxHeight) h = kMaxHeight;
float h = std::min(log10(scope_[i] * 256.0) * F_ * 0.5, kMaxHeight * 1.0);
if (h > bar_height_[i]) {
bar_height_[i] = h;
if (h > peak_height_[i]) {
peak_height_[i] = h;
peak_speed_[i] = 0.01;
@ -75,13 +71,11 @@ void TurbineAnalyzer::analyze(QPainter& p, const Scope& scope, bool new_frame) {
}
peak_handling:
if (peak_height_[i] > 0.0) {
peak_height_[i] -= peak_speed_[i];
peak_speed_[i] *= F_peakSpeed_; // 1.12
if (peak_height_[i] < bar_height_[i]) peak_height_[i] = bar_height_[i];
if (peak_height_[i] < 0.0) peak_height_[i] = 0.0;
peak_height_[i] =
std::max(0.0f, std::max(bar_height_[i], peak_height_[i]));
}
}

View File

@ -79,6 +79,9 @@ GlobalShortcuts::GlobalShortcuts(QWidget* parent)
SIGNAL(Love()));
AddShortcut("ban_last_fm_scrobbling", tr("Ban (Last.fm scrobbling)"),
SIGNAL(Ban()));
AddShortcut("remove_current_song_from_playlist",
tr("Remove current song from playlist"),
SIGNAL(RemoveCurrentSong()));
AddRatingShortcut("rate_zero_star", tr("Rate the current song 0 stars"),
rating_signals_mapper_, 0);

View File

@ -82,6 +82,7 @@ signals:
void ToggleScrobbling();
void Love();
void Ban();
void RemoveCurrentSong();
private:
void AddShortcut(const QString& id, const QString& name, const char* signal,

View File

@ -23,6 +23,7 @@
#include <QStringList>
#include <functional>
#include <limits>
// boost::multi_index still relies on these being in the global namespace.

View File

@ -80,6 +80,8 @@ void RegisterMetaTypes() {
"PlaylistSequence::RepeatMode");
qRegisterMetaType<PlaylistSequence::ShuffleMode>(
"PlaylistSequence::ShuffleMode");
qRegisterMetaType<QAbstractSocket::SocketState>(
"QAbstractSocket::SocketState");
qRegisterMetaType<QList<PodcastEpisode>>("QList<PodcastEpisode>");
qRegisterMetaType<QList<Podcast>>("QList<Podcast>");
qRegisterMetaType<QList<QNetworkCookie>>("QList<QNetworkCookie>");

View File

@ -334,6 +334,7 @@ void Player::PreviousItem(Engine::TrackChangeFlags change) {
app_->playlist_manager()->active()->set_current_row(i);
if (i == -1) {
Stop();
PlayAt(i, change, true);
return;
}

View File

@ -273,6 +273,10 @@ bool Song::is_unavailable() const { return d->unavailable_; }
int Song::id() const { return d->id_; }
const QString& Song::title() const { return d->title_; }
const QString& Song::album() const { return d->album_; }
const QString& Song::effective_album() const {
// This value is useful for singles, which are one-track albums on their own.
return d->album_.isEmpty() ? d->title_ : d->album_;
}
const QString& Song::artist() const { return d->artist_; }
const QString& Song::albumartist() const { return d->albumartist_; }
const QString& Song::effective_albumartist() const {
@ -577,7 +581,7 @@ void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const {
pb->set_filesize(d->filesize_);
pb->set_suspicious_tags(d->suspicious_tags_);
pb->set_art_automatic(DataCommaSizeFromQString(d->art_automatic_));
pb->set_type(static_cast< ::pb::tagreader::SongMetadata_Type>(d->filetype_));
pb->set_type(static_cast<pb::tagreader::SongMetadata_Type>(d->filetype_));
}
void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) {
@ -932,8 +936,9 @@ void Song::BindToQuery(QSqlQuery* query) const {
if (Application::kIsPortable &&
Utilities::UrlOnSameDriveAsClementine(d->url_)) {
query->bindValue(":filename", Utilities::GetRelativePathToClementineBin(
d->url_).toEncoded());
query->bindValue(
":filename",
Utilities::GetRelativePathToClementineBin(d->url_).toEncoded());
} else {
query->bindValue(":filename", d->url_.toEncoded());
}
@ -973,7 +978,8 @@ void Song::BindToQuery(QSqlQuery* query) const {
query->bindValue(":grouping", strval(d->grouping_));
query->bindValue(":lyrics", strval(d->lyrics_));
query->bindValue(":originalyear", intval(d->originalyear_));
query->bindValue(":effective_originalyear", intval(this->effective_originalyear()));
query->bindValue(":effective_originalyear",
intval(this->effective_originalyear()));
#undef intval
#undef notnullintval
@ -1074,9 +1080,9 @@ bool Song::IsMetadataEqual(const Song& other) const {
d->performer_ == other.d->performer_ &&
d->grouping_ == other.d->grouping_ && d->track_ == other.d->track_ &&
d->disc_ == other.d->disc_ && qFuzzyCompare(d->bpm_, other.d->bpm_) &&
d->year_ == other.d->year_ && d->originalyear_ == other.d->originalyear_ &&
d->genre_ == other.d->genre_ &&
d->comment_ == other.d->comment_ &&
d->year_ == other.d->year_ &&
d->originalyear_ == other.d->originalyear_ &&
d->genre_ == other.d->genre_ && d->comment_ == other.d->comment_ &&
d->compilation_ == other.d->compilation_ &&
d->beginning_ == other.d->beginning_ &&
length_nanosec() == other.length_nanosec() &&
@ -1084,8 +1090,7 @@ bool Song::IsMetadataEqual(const Song& other) const {
d->samplerate_ == other.d->samplerate_ &&
d->art_automatic_ == other.d->art_automatic_ &&
d->art_manual_ == other.d->art_manual_ &&
d->rating_ == other.d->rating_ &&
d->cue_path_ == other.d->cue_path_ &&
d->rating_ == other.d->rating_ && d->cue_path_ == other.d->cue_path_ &&
d->lyrics_ == other.d->lyrics_;
}
@ -1123,12 +1128,14 @@ bool Song::IsOnSameAlbum(const Song& other) const {
if (is_compilation() && album() == other.album()) return true;
return album() == other.album() && artist() == other.artist();
return effective_album() == other.effective_album() &&
effective_albumartist() == other.effective_albumartist();
}
QString Song::AlbumKey() const {
return QString("%1|%2|%3").arg(is_compilation() ? "_compilation" : artist(),
has_cue() ? cue_path() : "", album());
return QString("%1|%2|%3")
.arg(is_compilation() ? "_compilation" : effective_albumartist(),
has_cue() ? cue_path() : "", effective_album());
}
void Song::ToXesam(QVariantMap* map) const {

View File

@ -160,6 +160,7 @@ class Song {
const QString& title() const;
const QString& album() const;
const QString& effective_album() const;
const QString& artist() const;
const QString& albumartist() const;
const QString& effective_albumartist() const;

View File

@ -34,20 +34,23 @@
#include "config.h"
#include "core/logging.h"
#include "core/player.h"
#include "core/utilities.h"
#include "core/signalchecker.h"
#include "core/song.h"
#include "core/tagreaderclient.h"
#include "core/timeconstants.h"
#include "core/waitforsignal.h"
#include "internet/lastfm/fixlastfm.h"
#include "internet/core/internetmodel.h"
#include "internet/podcasts/podcastparser.h"
#include "internet/podcasts/podcastservice.h"
#include "internet/podcasts/podcasturlloader.h"
#include "library/librarybackend.h"
#include "library/sqlrow.h"
#include "playlistparsers/cueparser.h"
#include "playlistparsers/parserbase.h"
#include "playlistparsers/playlistparser.h"
#include "internet/podcasts/podcastparser.h"
#include "internet/podcasts/podcastservice.h"
#include "internet/podcasts/podcasturlloader.h"
#include "utilities.h"
#ifdef HAVE_AUDIOCD
#include <gst/audio/gstaudiocdsrc.h>
@ -115,6 +118,11 @@ SongLoader::Result SongLoader::Load(const QUrl& url) {
return Success;
}
// It could be a playlist, we give it a shot.
if (LoadRemotePlaylist(url_)) {
return Success;
}
url_ = PodcastUrlLoader::FixPodcastUrl(url_);
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
@ -144,10 +152,10 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString& filename) {
SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD
CddaSongLoader* cdda_song_loader = new CddaSongLoader;
connect(cdda_song_loader, SIGNAL(SongsDurationLoaded(SongList)),
this, SLOT(AudioCDTracksLoadedSlot(SongList)));
connect(cdda_song_loader, SIGNAL(SongsMetadataLoaded(SongList)),
this, SLOT(AudioCDTracksTagsLoaded(SongList)));
connect(cdda_song_loader, SIGNAL(SongsDurationLoaded(SongList)), this,
SLOT(AudioCDTracksLoadedSlot(SongList)));
connect(cdda_song_loader, SIGNAL(SongsMetadataLoaded(SongList)), this,
SLOT(AudioCDTracksTagsLoaded(SongList)));
cdda_song_loader->LoadSongs();
return Success;
#else // HAVE_AUDIOCD
@ -194,8 +202,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString& filename) {
}
// It's not in the database, load it asynchronously.
preload_func_ =
std::bind(&SongLoader::LoadLocalAsync, this, filename);
preload_func_ = std::bind(&SongLoader::LoadLocalAsync, this, filename);
return BlockingLoadRequired;
}
@ -217,8 +224,8 @@ void SongLoader::LoadLocalAsync(const QString& filename) {
if (!parser) {
// Check the file extension as well, maybe the magic failed, or it was a
// basic M3U file which is just a plain list of filenames.
parser = playlist_parser_->
ParserForExtension(QFileInfo(filename).suffix().toLower());
parser = playlist_parser_->ParserForExtension(
QFileInfo(filename).suffix().toLower());
}
if (parser) {
@ -410,8 +417,7 @@ void SongLoader::LoadRemote() {
// Add a probe to the sink so we can capture the data if it's a playlist
GstPad* pad = gst_element_get_static_pad(fakesink, "sink");
gst_pad_add_probe(
pad, GST_PAD_PROBE_TYPE_BUFFER, &DataReady, this, NULL);
gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, &DataReady, this, NULL);
gst_object_unref(pad);
QEventLoop loop;
@ -447,12 +453,11 @@ void SongLoader::TypeFound(GstElement*, uint, GstCaps* caps, void* self) {
instance->StopTypefindAsync(true);
}
GstPadProbeReturn SongLoader::DataReady(
GstPad*, GstPadProbeInfo* info, gpointer self) {
GstPadProbeReturn SongLoader::DataReady(GstPad*, GstPadProbeInfo* info,
gpointer self) {
SongLoader* instance = reinterpret_cast<SongLoader*>(self);
if (instance->state_ == Finished)
return GST_PAD_PROBE_OK;
if (instance->state_ == Finished) return GST_PAD_PROBE_OK;
GstBuffer* buffer = gst_pad_probe_info_get_buffer(info);
GstMapInfo map;
@ -617,3 +622,61 @@ void SongLoader::StopTypefindAsync(bool success) {
metaObject()->invokeMethod(this, "StopTypefind", Qt::QueuedConnection);
}
bool SongLoader::LoadRemotePlaylist(const QUrl& url) {
// This function makes a remote request for the given URL and, if its MIME
// type corresponds to a known playlist type, saves the content to a
// temporary file, loads it, and returns true.
// If the URL does not point to a playlist file we could handle,
// it returns false.
NetworkAccessManager manager;
QNetworkRequest req = QNetworkRequest(url);
// Getting headers:
QNetworkReply* const headers_reply = manager.head(req);
WaitForSignal(headers_reply, SIGNAL(finished()));
if (headers_reply->error() != QNetworkReply::NoError) {
qLog(Error) << url.toString() << headers_reply->errorString();
return false;
}
// Now we check if there is a parser that can handle that MIME type.
QString mime_type =
headers_reply->header(QNetworkRequest::ContentTypeHeader).toString();
ParserBase* const parser = playlist_parser_->ParserForMimeType(mime_type);
if (parser == nullptr) {
qLog(Debug) << url.toString() << "seems to not be a playlist";
return false;
}
// We know it is a playlist!
// Getting its contents:
QNetworkReply* const data_reply = manager.get(req);
WaitForSignal(data_reply, SIGNAL(finished()));
if (data_reply->error() != QNetworkReply::NoError) {
qLog(Error) << url.toString() << data_reply->errorString();
return false;
}
// Save them to a temporary file...
QString playlist_filename =
Utilities::SaveToTemporaryFile(data_reply->readAll());
if (playlist_filename.isEmpty()) {
qLog(Error) << url.toString()
<< "could not write contents to temporary file";
return false;
}
qLog(Debug) << url.toString() << "with MIME" << mime_type << "loading from"
<< playlist_filename;
// ...and load it.
LoadPlaylist(parser, playlist_filename);
QFile(playlist_filename).remove();
return true;
}

View File

@ -79,7 +79,7 @@ class SongLoader : public QObject {
void LoadMetadataBlocking();
Result LoadAudioCD();
signals:
signals:
void AudioCDTracksLoaded();
void LoadAudioCDFinished(bool success);
void LoadRemoteFinished();
@ -93,7 +93,12 @@ class SongLoader : public QObject {
#endif // HAVE_AUDIOCD
private:
enum State { WaitingForType, WaitingForMagic, WaitingForData, Finished, };
enum State {
WaitingForType,
WaitingForMagic,
WaitingForData,
Finished,
};
Result LoadLocal(const QString& filename);
void LoadLocalAsync(const QString& filename);
@ -105,6 +110,7 @@ class SongLoader : public QObject {
void AddAsRawStream();
void LoadRemote();
bool LoadRemotePlaylist(const QUrl& url);
// GStreamer callbacks
static void TypeFound(GstElement* typefind, uint probability, GstCaps* caps,

View File

@ -100,7 +100,8 @@ QString PrettyTime(int seconds) {
QString ret;
if (hours)
ret.sprintf("%d:%02d:%02d", hours, minutes, seconds); // NOLINT(runtime/printf)
ret.sprintf("%d:%02d:%02d", hours, minutes,
seconds); // NOLINT(runtime/printf)
else
ret.sprintf("%d:%02d", minutes, seconds); // NOLINT(runtime/printf)
@ -161,11 +162,15 @@ QString PrettySize(quint64 bytes) {
if (bytes <= 1000)
ret = QString::number(bytes) + " bytes";
else if (bytes <= 1000 * 1000)
ret.sprintf("%.1f KB", static_cast<float>(bytes) / 1000); // NOLINT(runtime/printf)
ret.sprintf("%.1f KB",
static_cast<float>(bytes) / 1000); // NOLINT(runtime/printf)
else if (bytes <= 1000 * 1000 * 1000)
ret.sprintf("%.1f MB", static_cast<float>(bytes) / (1000 * 1000)); // NOLINT(runtime/printf)
ret.sprintf("%.1f MB", static_cast<float>(bytes) /
(1000 * 1000)); // NOLINT(runtime/printf)
else
ret.sprintf("%.1f GB", static_cast<float>(bytes) / (1000 * 1000 * 1000)); // NOLINT(runtime/printf)
ret.sprintf("%.1f GB",
static_cast<float>(bytes) /
(1000 * 1000 * 1000)); // NOLINT(runtime/printf)
}
return ret;
}
@ -231,6 +236,23 @@ QString GetTemporaryFileName() {
return file;
}
QString SaveToTemporaryFile(const QByteArray& data) {
QTemporaryFile tempfile;
tempfile.setAutoRemove(false);
if (!tempfile.open()) {
return QString();
}
if (tempfile.write(data) != data.size()) {
tempfile.remove();
return QString();
}
tempfile.close();
return tempfile.fileName();
}
bool RemoveRecursive(const QString& path) {
QDir dir(path);
for (const QString& child :
@ -562,19 +584,13 @@ bool ParseUntilElement(QXmlStreamReader* reader, const QString& name) {
}
QDateTime ParseRFC822DateTime(const QString& text) {
QRegExp regexp("(\\d{1,2}) (\\w{3,12}) (\\d+) (\\d{1,2}):(\\d{1,2}):(\\d{1,2})");
QRegExp regexp(
"(\\d{1,2}) (\\w{3,12}) (\\d+) (\\d{1,2}):(\\d{1,2}):(\\d{1,2})");
if (regexp.indexIn(text) == -1) {
return QDateTime();
}
enum class MatchNames {
DAYS = 1,
MONTHS,
YEARS,
HOURS,
MINUTES,
SECONDS
};
enum class MatchNames { DAYS = 1, MONTHS, YEARS, HOURS, MINUTES, SECONDS };
QMap<QString, int> monthmap;
monthmap["Jan"] = 1;
@ -610,7 +626,7 @@ QDateTime ParseRFC822DateTime(const QString& text) {
regexp.cap(static_cast<int>(MatchNames::MINUTES)).toInt(),
regexp.cap(static_cast<int>(MatchNames::SECONDS)).toInt());
return QDateTime (date, time);
return QDateTime(date, time);
}
const char* EnumToString(const QMetaObject& meta, const char* name, int value) {

View File

@ -56,6 +56,8 @@ quint64 FileSystemFreeSpace(const QString& path);
QString MakeTempDir(const QString template_name = QString());
QString GetTemporaryFileName();
QString SaveToTemporaryFile(const QByteArray& data);
bool RemoveRecursive(const QString& path);
bool CopyRecursive(const QString& source, const QString& destination);
bool Copy(QIODevice* source, QIODevice* destination);

View File

@ -17,6 +17,7 @@
#include "config.h"
#include <functional>
#include <memory>
#include <QFile>

View File

@ -87,7 +87,6 @@ void OAuthenticator::StartAuthorisation(const QString& oauth_endpoint,
void OAuthenticator::RedirectArrived(LocalRedirectServer* server, QUrl url) {
server->deleteLater();
QUrl request_url = server->request_url();
qLog(Debug) << Q_FUNC_INFO << request_url;
RequestAccessToken(QUrlQuery(request_url).queryItemValue("code").toUtf8(), url);
}
@ -146,12 +145,10 @@ void OAuthenticator::FetchAccessTokenFinished(QNetworkReply* reply) {
return;
}
QJsonObject json_result = json_document.object();
qLog(Debug) << json_result;
access_token_ = json_result["access_token"].toString();
refresh_token_ = json_result["refresh_token"].toString();
SetExpiryTime(json_result["expires_in"].toInt());
QJsonObject result = json_document.object();
access_token_ = result["access_token"].toString();
refresh_token_ = result["refresh_token"].toString();
SetExpiryTime(result["expires_in"].toInt());
emit Finished();
}

View File

@ -24,13 +24,11 @@
#include <QCoreApplication>
#include <QDesktopServices>
#include <QMenu>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QXmlStreamReader>
#include <QtDebug>
#include "intergalacticfmurlhandler.h"
#include "internet/core/internetmodel.h"
#include "core/application.h"
#include "core/closure.h"
#include "core/logging.h"
@ -40,6 +38,8 @@
#include "core/utilities.h"
#include "globalsearch/globalsearch.h"
#include "globalsearch/intergalacticfmsearchprovider.h"
#include "intergalacticfmurlhandler.h"
#include "internet/core/internetmodel.h"
#include "ui/iconloader.h"
const int IntergalacticFMServiceBase::kStreamsCacheDurationSecs =
@ -276,6 +276,6 @@ IntergalacticFMService::IntergalacticFMService(Application* app,
InternetModel* parent)
: IntergalacticFMServiceBase(
app, parent, "Intergalactic FM",
QUrl("https://intergalacticfm.com/channels.xml"),
QUrl("http://intergalacticfm.com"), QUrl(),
QUrl("https://www.intergalactic.fm/channels.xml"),
QUrl("https://www.intergalactic.fm"), QUrl(),
IconLoader::Load("intergalacticfm", IconLoader::Provider)) {}

View File

@ -63,7 +63,7 @@ const char* SoundCloudService::kHomepage = "http://soundcloud.com/";
const int SoundCloudService::kSearchDelayMsec = 400;
const int SoundCloudService::kSongSearchLimit = 100;
const int SoundCloudService::kSongSimpleSearchLimit = 10;
const int SoundCloudService::kSongSimpleSearchLimit = 100;
typedef QPair<QString, QString> Param;
@ -325,9 +325,8 @@ void SoundCloudService::Search(const QString& text, bool now) {
void SoundCloudService::DoSearch() {
ClearSearchResults();
QList<Param> parameters;
parameters << Param("q", pending_search_);
QNetworkReply* reply = CreateRequest("/tracks", parameters);
QList<Param> parameters; parameters << Param("q", pending_search_) << Param("limit", QString::number(kSongSearchLimit));
QNetworkReply* reply = CreateRequest("tracks", parameters);
const int id = next_pending_search_id_++;
NewClosure(reply, SIGNAL(finished()), this,
SLOT(SearchFinished(QNetworkReply*, int)), reply, id);
@ -355,8 +354,8 @@ void SoundCloudService::ClearSearchResults() {
int SoundCloudService::SimpleSearch(const QString& text) {
QList<Param> parameters;
parameters << Param("q", text);
QNetworkReply* reply = CreateRequest("/tracks", parameters);
parameters << Param("q", text) << Param("limit", QString::number(kSongSimpleSearchLimit));
QNetworkReply* reply = CreateRequest("tracks", parameters);
const int id = next_pending_search_id_++;
NewClosure(reply, SIGNAL(finished()), this,
SLOT(SimpleSearchFinished(QNetworkReply*, int)), reply, id);

View File

@ -125,7 +125,7 @@ SpotifyService::~SpotifyService() {
}
QStandardItem* SpotifyService::CreateRootItem() {
root_ = new QStandardItem(IconLoader::Load("spotify", IconLoader::Provider),
root_ = new QStandardItem(IconLoader::Load("spotify", IconLoader::Provider),
kServiceName);
root_->setData(true, InternetModel::Role_CanLazyLoad);
return root_;
@ -395,6 +395,43 @@ void SpotifyService::AddSongsToStarred(const QList<QUrl>& songs_urls) {
server_->AddSongsToStarred(songs_urls);
}
void SpotifyService::InitSearch() {
search_ = new QStandardItem(IconLoader::Load("edit-find", IconLoader::Base),
tr("Search results"));
search_->setToolTip(
tr("Start typing something on the search box above to "
"fill this search results list"));
search_->setData(Type_SearchResults, InternetModel::Role_Type);
search_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
starred_ = new QStandardItem(IconLoader::Load("star-on", IconLoader::Other),
tr("Starred"));
starred_->setData(Type_StarredPlaylist, InternetModel::Role_Type);
starred_->setData(true, InternetModel::Role_CanLazyLoad);
starred_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
starred_->setData(true, InternetModel::Role_CanBeModified);
inbox_ = new QStandardItem(IconLoader::Load("mail-message", IconLoader::Base),
tr("Inbox"));
inbox_->setData(Type_InboxPlaylist, InternetModel::Role_Type);
inbox_->setData(true, InternetModel::Role_CanLazyLoad);
inbox_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
toplist_ = new QStandardItem(QIcon(), tr("Top tracks"));
toplist_->setData(Type_Toplist, InternetModel::Role_Type);
toplist_->setData(true, InternetModel::Role_CanLazyLoad);
toplist_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
root_->appendRow(search_);
root_->appendRow(toplist_);
root_->appendRow(starred_);
root_->appendRow(inbox_);
}
void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) {
if (login_task_id_) {
app_->task_manager()->SetTaskFinished(login_task_id_);
@ -403,41 +440,7 @@ void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) {
// Create starred and inbox playlists if they're not here already
if (!search_) {
search_ =
new QStandardItem(IconLoader::Load("edit-find", IconLoader::Base),
tr("Search results"));
search_->setToolTip(
tr("Start typing something on the search box above to "
"fill this search results list"));
search_->setData(Type_SearchResults, InternetModel::Role_Type);
search_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
starred_ = new QStandardItem(IconLoader::Load("star-on", IconLoader::Other),
tr("Starred"));
starred_->setData(Type_StarredPlaylist, InternetModel::Role_Type);
starred_->setData(true, InternetModel::Role_CanLazyLoad);
starred_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
starred_->setData(true, InternetModel::Role_CanBeModified);
inbox_ = new QStandardItem(IconLoader::Load("mail-message",
IconLoader::Base), tr("Inbox"));
inbox_->setData(Type_InboxPlaylist, InternetModel::Role_Type);
inbox_->setData(true, InternetModel::Role_CanLazyLoad);
inbox_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
toplist_ = new QStandardItem(QIcon(), tr("Top tracks"));
toplist_->setData(Type_Toplist, InternetModel::Role_Type);
toplist_->setData(true, InternetModel::Role_CanLazyLoad);
toplist_->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
root_->appendRow(search_);
root_->appendRow(toplist_);
root_->appendRow(starred_);
root_->appendRow(inbox_);
InitSearch();
} else {
// Always reset starred playlist
// TODO: might be improved by including starred playlist in the response,
@ -618,9 +621,9 @@ QList<QAction*> SpotifyService::playlistitem_actions(const Song& song) {
playlistitem_actions_.append(add_to_starred);
// Create a menu with 'add to playlist' actions for each Spotify playlist
QAction* add_to_playlists = new QAction(IconLoader::Load("list-add",
IconLoader::Base),
tr("Add to Spotify playlists"), this);
QAction* add_to_playlists =
new QAction(IconLoader::Load("list-add", IconLoader::Base),
tr("Add to Spotify playlists"), this);
QMenu* playlists_menu = new QMenu();
for (const QStandardItem* playlist_item : playlists_) {
if (!playlist_item->data(InternetModel::Role_CanBeModified).toBool()) {
@ -665,7 +668,7 @@ void SpotifyService::EnsureMenuCreated() {
playlist_context_menu_->addActions(GetPlaylistActions());
playlist_context_menu_->addSeparator();
playlist_sync_action_ = playlist_context_menu_->addAction(
IconLoader::Load("view-refresh", IconLoader::Base),
IconLoader::Load("view-refresh", IconLoader::Base),
tr("Make playlist available offline"), this, SLOT(SyncPlaylist()));
get_url_to_share_playlist_ = playlist_context_menu_->addAction(
tr("Get a URL to share this playlist"), this,
@ -677,9 +680,8 @@ void SpotifyService::EnsureMenuCreated() {
song_context_menu_->addActions(GetPlaylistActions());
song_context_menu_->addSeparator();
remove_from_playlist_ = song_context_menu_->addAction(
IconLoader::Load("list-remove", IconLoader::Base),
tr("Remove from playlist"), this,
SLOT(RemoveCurrentFromPlaylist()));
IconLoader::Load("list-remove", IconLoader::Base),
tr("Remove from playlist"), this, SLOT(RemoveCurrentFromPlaylist()));
song_context_menu_->addAction(tr("Get a URL to share this Spotify song"),
this, SLOT(GetCurrentSongUrlToShare()));
song_context_menu_->addSeparator();
@ -765,6 +767,10 @@ void SpotifyService::SearchResults(
ClearSearchResults();
// Must initialize search pointer if it is nullptr
if (!search_) {
InitSearch();
}
// Fill results list
for (const Song& song : songs) {
QStandardItem* child = CreateSongItem(song);
@ -848,10 +854,10 @@ void SpotifyService::ItemDoubleClicked(QStandardItem* item) {}
void SpotifyService::DropMimeData(const QMimeData* data,
const QModelIndex& index) {
QModelIndex playlist_root_index = index;
QVariant q_playlist_type = playlist_root_index.data(InternetModel::Role_Type);
if (!q_playlist_type.isValid() || q_playlist_type.toInt() == InternetModel::Type_Track) {
if (!q_playlist_type.isValid() ||
q_playlist_type.toInt() == InternetModel::Type_Track) {
// In case song was dropped on a playlist item, not on the playlist
// title/root element
playlist_root_index = index.parent();
@ -864,7 +870,8 @@ void SpotifyService::DropMimeData(const QMimeData* data,
if (playlist_type == Type_StarredPlaylist) {
AddSongsToStarred(data->urls());
} else if (playlist_type == InternetModel::Type_UserPlaylist) {
QVariant q_playlist_index = playlist_root_index.data(Role_UserPlaylistIndex);
QVariant q_playlist_index =
playlist_root_index.data(Role_UserPlaylistIndex);
if (!q_playlist_index.isValid()) return;
AddSongsToUserPlaylist(q_playlist_index.toInt(), data->urls());
}
@ -916,8 +923,7 @@ void SpotifyService::SyncPlaylistProgress(
}
QAction* SpotifyService::GetNewShowConfigAction() {
QAction* action = new QAction(IconLoader::Load("configure",
IconLoader::Base),
QAction* action = new QAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure Spotify..."), this);
connect(action, SIGNAL(triggered()), this, SLOT(ShowConfig()));
return action;

View File

@ -120,8 +120,8 @@ class SpotifyService : public InternetService {
// the pointer (or adding it to menu or anything else that will take ownership
// of it)
QAction* GetNewShowConfigAction();
void InitSearch();
void ClearSearchResults();
QStandardItem* PlaylistBySpotifyIndex(int index) const;
bool DoPlaylistsDiffer(const pb::spotify::Playlists& response) const;

View File

@ -20,6 +20,8 @@
#include <QPushButton>
#include <functional>
// boost::multi_index still relies on these being in the global namespace.
using std::placeholders::_1;
using std::placeholders::_2;

View File

@ -520,29 +520,50 @@ QStringList LibraryBackend::GetAllArtists(const QueryOptions& opt) {
}
QStringList LibraryBackend::GetAllArtistsWithAlbums(const QueryOptions& opt) {
// Albums with 'albumartist' field set:
LibraryQuery query(opt);
query.SetColumnSpec("DISTINCT artist");
query.SetColumnSpec("DISTINCT albumartist");
query.AddCompilationRequirement(false);
query.AddWhere("album", "", "!=");
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query)) return QStringList();
// Albums with no 'albumartist' (extract 'artist'):
LibraryQuery query2(opt);
query2.SetColumnSpec("DISTINCT artist");
query2.AddCompilationRequirement(false);
query2.AddWhere("album", "", "!=");
query2.AddWhere("albumartist", "", "=");
QStringList ret;
while (query.Next()) {
ret << query.Value(0).toString();
{
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query) || !ExecQuery(&query2)) {
return QStringList();
}
}
return ret;
QSet<QString> artists;
while (query.Next()) {
artists << query.Value(0).toString();
}
while (query2.Next()) {
artists << query2.Value(0).toString();
}
return QStringList(artists.toList());
}
LibraryBackend::AlbumList LibraryBackend::GetAllAlbums(
const QueryOptions& opt) {
return GetAlbums(QString(), false, opt);
return GetAlbums(QString(), QString(), false, opt);
}
LibraryBackend::AlbumList LibraryBackend::GetAlbumsByArtist(
const QString& artist, const QueryOptions& opt) {
return GetAlbums(artist, false, opt);
return GetAlbums(artist, QString(), false, opt);
}
LibraryBackend::AlbumList LibraryBackend::GetAlbumsByAlbumArtist(
const QString& album_artist, const QueryOptions& opt) {
return GetAlbums(QString(), album_artist, false, opt);
}
SongList LibraryBackend::GetSongsByAlbum(const QString& album,
@ -689,7 +710,7 @@ SongList LibraryBackend::GetSongsByUrl(const QUrl& url) {
LibraryBackend::AlbumList LibraryBackend::GetCompilationAlbums(
const QueryOptions& opt) {
return GetAlbums(QString(), true, opt);
return GetAlbums(QString(), QString(), true, opt);
}
SongList LibraryBackend::GetCompilationSongs(const QString& album,
@ -827,58 +848,76 @@ void LibraryBackend::UpdateCompilations(QSqlQuery& find_songs,
}
LibraryBackend::AlbumList LibraryBackend::GetAlbums(const QString& artist,
const QString& album_artist,
bool compilation,
const QueryOptions& opt) {
AlbumList ret;
LibraryQuery query(opt);
query.SetColumnSpec(
"album, artist, compilation, sampler, art_automatic, "
"album, artist, albumartist, compilation, sampler, art_automatic, "
"art_manual, filename");
query.SetOrderBy("album");
if (compilation) {
query.AddCompilationRequirement(true);
} else if (!album_artist.isNull()) {
query.AddCompilationRequirement(false);
query.AddWhere("albumartist", album_artist);
} else if (!artist.isNull()) {
query.AddCompilationRequirement(false);
query.AddWhere("artist", artist);
}
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query)) return ret;
{
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query)) return ret;
}
QString last_album;
QString last_artist;
QString last_album_artist;
while (query.Next()) {
bool compilation = query.Value(2).toBool() | query.Value(3).toBool();
bool compilation = query.Value(3).toBool() | query.Value(4).toBool();
Album info;
info.artist = compilation ? QString() : query.Value(1).toString();
info.album_artist = compilation ? QString() : query.Value(2).toString();
info.album_name = query.Value(0).toString();
info.art_automatic = query.Value(4).toString();
info.art_manual = query.Value(5).toString();
info.first_url = QUrl::fromEncoded(query.Value(6).toByteArray());
info.art_automatic = query.Value(5).toString();
info.art_manual = query.Value(6).toString();
info.first_url = QUrl::fromEncoded(query.Value(7).toByteArray());
if (info.artist == last_artist && info.album_name == last_album) continue;
if ((info.artist == last_artist ||
info.album_artist == last_album_artist) &&
info.album_name == last_album)
continue;
ret << info;
last_album = info.album_name;
last_artist = info.artist;
last_album_artist = info.album_artist;
}
return ret;
}
LibraryBackend::Album LibraryBackend::GetAlbumArt(const QString& artist,
const QString& albumartist,
const QString& album) {
Album ret;
ret.album_name = album;
ret.artist = artist;
ret.album_artist = albumartist;
LibraryQuery query = LibraryQuery(QueryOptions());
query.SetColumnSpec("art_automatic, art_manual, filename");
query.AddWhere("artist", artist);
if (!albumartist.isEmpty()) {
query.AddWhere("albumartist", albumartist);
} else if (!artist.isEmpty()) {
query.AddWhere("artist", artist);
}
query.AddWhere("album", album);
QMutexLocker l(db_->Mutex());
@ -894,14 +933,17 @@ LibraryBackend::Album LibraryBackend::GetAlbumArt(const QString& artist,
}
void LibraryBackend::UpdateManualAlbumArtAsync(const QString& artist,
const QString& albumartist,
const QString& album,
const QString& art) {
metaObject()->invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection,
Q_ARG(QString, artist), Q_ARG(QString, album),
Q_ARG(QString, artist),
Q_ARG(QString, albumartist), Q_ARG(QString, album),
Q_ARG(QString, art));
}
void LibraryBackend::UpdateManualAlbumArt(const QString& artist,
const QString& albumartist,
const QString& album,
const QString& art) {
QMutexLocker l(db_->Mutex());
@ -911,7 +953,12 @@ void LibraryBackend::UpdateManualAlbumArt(const QString& artist,
LibraryQuery query;
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("album", album);
if (!artist.isNull()) query.AddWhere("artist", artist);
if (!albumartist.isNull()) {
query.AddWhere("albumartist", albumartist);
} else if (!artist.isNull()) {
query.AddWhere("artist", artist);
}
if (!ExecQuery(&query)) return;
@ -927,13 +974,21 @@ void LibraryBackend::UpdateManualAlbumArt(const QString& artist,
QString(
"UPDATE %1 SET art_manual = :art"
" WHERE album = :album AND unavailable = 0").arg(songs_table_));
if (!artist.isNull()) sql += " AND artist = :artist";
if (!albumartist.isNull()) {
sql += " AND albumartist = :albumartist";
} else if (!artist.isNull()) {
sql += " AND artist = :artist";
}
QSqlQuery q(db);
q.prepare(sql);
q.bindValue(":art", art);
q.bindValue(":album", album);
if (!artist.isNull()) q.bindValue(":artist", artist);
if (!albumartist.isNull()) {
q.bindValue(":albumartist", albumartist);
} else if (!artist.isNull()) {
q.bindValue(":artist", artist);
}
q.exec();
db_->CheckErrors(q);

View File

@ -41,16 +41,22 @@ class LibraryBackendInterface : public QObject {
struct Album {
Album() {}
Album(const QString& _artist, const QString& _album_name,
const QString& _art_automatic, const QString& _art_manual,
const QUrl& _first_url)
Album(const QString& _artist, const QString& _album_artist,
const QString& _album_name, const QString& _art_automatic,
const QString& _art_manual, const QUrl& _first_url)
: artist(_artist),
album_artist(_album_artist),
album_name(_album_name),
art_automatic(_art_automatic),
art_manual(_art_manual),
first_url(_first_url) {}
const QString& effective_albumartist() const {
return album_artist.isEmpty() ? artist : album_artist;
}
QString artist;
QString album_artist;
QString album_name;
QString art_automatic;
@ -92,9 +98,11 @@ class LibraryBackendInterface : public QObject {
const QueryOptions& opt = QueryOptions()) = 0;
virtual void UpdateManualAlbumArtAsync(const QString& artist,
const QString& albumartist,
const QString& album,
const QString& art) = 0;
virtual Album GetAlbumArt(const QString& artist, const QString& album) = 0;
virtual Album GetAlbumArt(const QString& artist, const QString& albumartist,
const QString& album) = 0;
virtual Song GetSongById(int id) = 0;
@ -157,11 +165,15 @@ class LibraryBackend : public LibraryBackendInterface {
AlbumList GetAllAlbums(const QueryOptions& opt = QueryOptions());
AlbumList GetAlbumsByArtist(const QString& artist,
const QueryOptions& opt = QueryOptions());
AlbumList GetAlbumsByAlbumArtist(const QString& albumartist,
const QueryOptions& opt = QueryOptions());
AlbumList GetCompilationAlbums(const QueryOptions& opt = QueryOptions());
void UpdateManualAlbumArtAsync(const QString& artist, const QString& album,
const QString& art);
Album GetAlbumArt(const QString& artist, const QString& album);
void UpdateManualAlbumArtAsync(const QString& artist,
const QString& albumartist,
const QString& album, const QString& art);
Album GetAlbumArt(const QString& artist, const QString& albumartist,
const QString& album);
Song GetSongById(int id);
SongList GetSongsById(const QList<int>& ids);
@ -197,8 +209,8 @@ class LibraryBackend : public LibraryBackendInterface {
void MarkSongsUnavailable(const SongList& songs, bool unavailable = true);
void AddOrUpdateSubdirs(const SubdirectoryList& subdirs);
void UpdateCompilations();
void UpdateManualAlbumArt(const QString& artist, const QString& album,
const QString& art);
void UpdateManualAlbumArt(const QString& artist, const QString& albumartist,
const QString& album, const QString& art);
void ForceCompilation(const QString& album, const QList<QString>& artists,
bool on);
void IncrementPlayCount(int id);
@ -236,7 +248,8 @@ signals:
void UpdateCompilations(QSqlQuery& find_songs, QSqlQuery& update,
SongList& deleted_songs, SongList& added_songs,
const QString& album, int sampler);
AlbumList GetAlbums(const QString& artist, bool compilation = false,
AlbumList GetAlbums(const QString& artist, const QString& album_artist,
bool compilation = false,
const QueryOptions& opt = QueryOptions());
SubdirectoryList SubdirsInDirectory(int id, QSqlDatabase& db);

View File

@ -270,17 +270,20 @@ QRect MoodbarProxyStyle::subControlRect(ComplexControl cc,
case SC_SliderHandle: {
const QStyleOptionSlider* slider_opt =
qstyleoption_cast<const QStyleOptionSlider*>(opt);
int x = 0;
int x_offset = 0;
/* slider_opt->{maximum,minimum} can have the value 0 (their default
values), so this check avoids a division by 0. */
if (slider_opt->maximum > slider_opt->minimum) {
x = (slider_opt->sliderValue - slider_opt->minimum) *
(opt->rect.width() - kArrowWidth) /
(slider_opt->maximum - slider_opt->minimum);
qint64 slider_delta = slider_opt->sliderValue - slider_opt->minimum;
qint64 slider_range = slider_opt->maximum - slider_opt->minimum;
int rectangle_effective_width = opt->rect.width() - kArrowWidth;
qint64 x = slider_delta * rectangle_effective_width / slider_range;
x_offset = static_cast<int>(x);
}
return QRect(QPoint(opt->rect.left() + x, opt->rect.top()),
return QRect(QPoint(opt->rect.left() + x_offset, opt->rect.top()),
QSize(kArrowWidth, kArrowHeight));
}

View File

@ -1592,6 +1592,8 @@ bool Playlist::removeRows(int row, int count, const QModelIndex& parent) {
// might have been invalidated.
RemoveItemsWithoutUndo(row, count);
undo_stack_->clear();
} else if (parent == QModelIndex()) {
RemoveItemsWithoutUndo(row, count);
} else {
undo_stack_->push(new PlaylistUndoCommands::RemoveItems(this, row, count));
}

View File

@ -67,10 +67,10 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
no_matches_palette.setColor(QPalette::Inactive, QPalette::WindowText,
no_matches_color);
no_matches_label_->setPalette(no_matches_palette);
// Remove QFrame border
ui_->toolbar->setStyleSheet("QFrame { border: 0px; }");
// Make it bold
QFont no_matches_font = no_matches_label_->font();
no_matches_font.setBold(true);
@ -273,9 +273,7 @@ void PlaylistContainer::PlaylistAdded(int id, const QString& name,
}
}
void PlaylistContainer::Started() {
starting_up_ = false;
}
void PlaylistContainer::Started() { starting_up_ = false; }
void PlaylistContainer::PlaylistClosed(int id) {
ui_->tab_bar->RemoveTab(id);
@ -436,7 +434,6 @@ bool PlaylistContainer::eventFilter(QObject* objectWatched, QEvent* event) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent* e = static_cast<QKeyEvent*>(event);
switch (e->key()) {
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:

View File

@ -196,7 +196,7 @@ void PlaylistManager::ItemsLoadedForSavePlaylist(QFuture<SongList> future,
parser_->Save(future.result(), filename, path_type);
}
void PlaylistManager::SaveWithUI(int id, const QString& suggested_filename) {
void PlaylistManager::SaveWithUI(int id, const QString& playlist_name) {
QSettings settings;
settings.beginGroup(Playlist::kSettingsGroup);
QString filename = settings.value("last_save_playlist").toString();
@ -205,10 +205,13 @@ void PlaylistManager::SaveWithUI(int id, const QString& suggested_filename) {
QString filter =
settings.value("last_save_filter", parser()->default_filter()).toString();
QString suggested_filename = playlist_name;
suggested_filename.replace(QRegExp("\\W"), "");
qLog(Debug) << "Using extension:" << extension;
// We want to use the playlist tab name as a default filename, but in the
// same directory as the last saved file.
// We want to use the playlist tab name (with disallowed characters removed)
// as a default filename, but in the same directory as the last saved file.
// Strip off filename components until we find something that's a folder
forever {
@ -237,11 +240,11 @@ void PlaylistManager::SaveWithUI(int id, const QString& suggested_filename) {
QFileInfo info(filename);
ParserBase* parser = parser_->ParserForExtension(info.suffix());
if (!parser) {
qLog(Warning) << "Unknown file extension:" << info.suffix();
filename = info.absolutePath() + "/" + info.fileName()
+ "." + parser_->default_extension();
info.setFile(filename);
filter = info.suffix();
qLog(Warning) << "Unknown file extension:" << info.suffix();
filename = info.absolutePath() + "/" + info.fileName() + "." +
parser_->default_extension();
info.setFile(filename);
filter = info.suffix();
}
int p = settings.value(Playlist::kPathType, Playlist::Path_Automatic).toInt();
@ -491,6 +494,10 @@ void PlaylistManager::RemoveItemsWithoutUndo(int id,
playlists_[id].p->RemoveItemsWithoutUndo(indices);
}
void PlaylistManager::RemoveCurrentSong() {
active()->removeRows(active()->current_index().row(), 1);
}
void PlaylistManager::InvalidateDeletedSongs() {
for (Playlist* playlist : GetAllPlaylists()) {
playlist->InvalidateDeletedSongs();

View File

@ -184,7 +184,7 @@ class PlaylistManager : public PlaylistManagerInterface {
void Load(const QString& filename);
void Save(int id, const QString& filename, Playlist::Path path_type);
// Display a file dialog to let user choose a file before saving the file
void SaveWithUI(int id, const QString& suggested_filename);
void SaveWithUI(int id, const QString& playlist_name);
void Rename(int id, const QString& new_name);
void Favorite(int id, bool favorite);
void Delete(int id);
@ -225,6 +225,8 @@ class PlaylistManager : public PlaylistManagerInterface {
// Removes items with given indices from the playlist. This operation is not
// undoable.
void RemoveItemsWithoutUndo(int id, const QList<int>& indices);
// Remove the current playing song
void RemoveCurrentSong();
private slots:
void SetActivePlaying();

View File

@ -138,16 +138,14 @@ PlaylistView::PlaylistView(QWidget* parent)
setStyle(style_);
setMouseTracking(true);
QIcon currenttrack_play = IconLoader::Load("currenttrack_play",
IconLoader::Other);
currenttrack_play_ = currenttrack_play.pixmap(currenttrack_play
.availableSizes()
.last());
QIcon currenttrack_pause = IconLoader::Load("currenttrack_pause",
IconLoader::Other);
currenttrack_pause_ = currenttrack_pause.pixmap(currenttrack_pause
.availableSizes()
.last());
QIcon currenttrack_play =
IconLoader::Load("currenttrack_play", IconLoader::Other);
currenttrack_play_ =
currenttrack_play.pixmap(currenttrack_play.availableSizes().last());
QIcon currenttrack_pause =
IconLoader::Load("currenttrack_pause", IconLoader::Other);
currenttrack_pause_ =
currenttrack_pause.pixmap(currenttrack_pause.availableSizes().last());
connect(header_, SIGNAL(sectionResized(int, int, int)), SLOT(SaveGeometry()));
connect(header_, SIGNAL(sectionMoved(int, int, int)), SLOT(SaveGeometry()));
@ -601,11 +599,11 @@ void PlaylistView::keyPressEvent(QKeyEvent* event) {
if (!model() || state() == QAbstractItemView::EditingState) {
QTreeView::keyPressEvent(event);
} else if (event == QKeySequence::Delete) {
RemoveSelected();
RemoveSelected(false);
event->accept();
#ifdef Q_OS_DARWIN
} else if (event->key() == Qt::Key_Backspace) {
RemoveSelected();
RemoveSelected(false);
event->accept();
#endif
} else if (event == QKeySequence::Copy) {
@ -619,6 +617,9 @@ void PlaylistView::keyPressEvent(QKeyEvent* event) {
event->key() == Qt::Key_Space) {
emit PlayPause();
event->accept();
} else if (event->key() == Qt::Key_Up) {
app_->player()->SeekTo(0);
event->accept();
} else if (event->key() == Qt::Key_Left) {
emit SeekBackward();
event->accept();
@ -644,7 +645,7 @@ void PlaylistView::contextMenuEvent(QContextMenuEvent* e) {
e->accept();
}
void PlaylistView::RemoveSelected() {
void PlaylistView::RemoveSelected(bool deleting_from_disk) {
int rows_removed = 0;
QItemSelection selection(selectionModel()->selection());
@ -661,7 +662,12 @@ void PlaylistView::RemoveSelected() {
for (const QItemSelectionRange& range : selection) {
if (range.top() < last_row) rows_removed += range.height();
model()->removeRows(range.top(), range.height(), range.parent());
if (!deleting_from_disk) {
model()->removeRows(range.top(), range.height(), range.topLeft());
} else {
model()->removeRows(range.top(), range.height(), QModelIndex());
}
}
int new_row = last_row - rows_removed;

View File

@ -74,7 +74,7 @@ class PlaylistView : public QTreeView {
void SetApplication(Application* app);
void SetItemDelegates(LibraryBackend* backend);
void SetPlaylist(Playlist* playlist);
void RemoveSelected();
void RemoveSelected(bool deleting_from_disk);
void SetReadOnlySettings(bool read_only) { read_only_settings_ = read_only; }
@ -246,7 +246,7 @@ signals:
int drop_indicator_row_;
bool drag_over_;
bool ratings_locked_; // To store Ratings section lock status
bool ratings_locked_; // To store Ratings section lock status
DynamicPlaylistControls* dynamic_controls_;

View File

@ -53,6 +53,17 @@ QStringList PlaylistParser::file_extensions() const {
return ret;
}
QStringList PlaylistParser::mime_types() const {
QStringList ret;
for (ParserBase* parser : parsers_) {
if (!parser->mime_type().isEmpty()) ret << parser->mime_type();
}
qStableSort(ret);
return ret;
}
QString PlaylistParser::filters() const {
QStringList filters;
QStringList all_extensions;
@ -91,6 +102,15 @@ ParserBase* PlaylistParser::ParserForExtension(const QString& suffix) const {
return nullptr;
}
ParserBase* PlaylistParser::ParserForMimeType(const QString& mime_type) const {
for (ParserBase* p : parsers_) {
if (!p->mime_type().isEmpty() &&
(QString::compare(p->mime_type(), mime_type, Qt::CaseInsensitive) == 0))
return p;
}
return nullptr;
}
ParserBase* PlaylistParser::ParserForMagic(const QByteArray& data,
const QString& mime_type) const {
for (ParserBase* p : parsers_) {

View File

@ -38,12 +38,15 @@ class PlaylistParser : public QObject {
QStringList file_extensions() const;
QString filters() const;
QStringList mime_types() const;
QString default_extension() const;
QString default_filter() const;
ParserBase* ParserForMagic(const QByteArray& data,
const QString& mime_type = QString()) const;
ParserBase* ParserForExtension(const QString& suffix) const;
ParserBase* ParserForMimeType(const QString& mime) const;
SongList LoadFromFile(const QString& filename) const;
SongList LoadFromDevice(QIODevice* device,

View File

@ -28,6 +28,7 @@ class PLSParser : public ParserBase {
QString name() const { return "PLS"; }
QStringList file_extensions() const { return QStringList() << "pls"; }
QString mime_type() const { return "audio/x-scpls"; }
bool TryMagic(const QByteArray& data) const;

View File

@ -14,14 +14,8 @@
#include "core/network.h"
namespace {
static const char* kSpotifySearchUrl = "https://api.spotify.com/v1/search";
static const char* kSpotifyArtistUrl = "https://api.spotify.com/v1/artists/%1";
} // namespace
namespace {
QString ExtractSpotifyId(const QString& spotify_uri) {
return spotify_uri.split(':')[2];
}
static const char* kSpotifyImagesUrl =
"https://data.clementine-player.org/fetchimages";
} // namespace
SpotifyImages::SpotifyImages() : network_(new NetworkAccessManager) {}
@ -35,65 +29,42 @@ void SpotifyImages::FetchInfo(int id, const Song& metadata) {
}
// Fetch artist id.
QUrl search_url(kSpotifySearchUrl);
QUrlQuery search_url_query;
search_url_query.addQueryItem("q", metadata.artist());
search_url_query.addQueryItem("type", "artist");
search_url_query.addQueryItem("limit", "1");
search_url.setQuery(search_url_query);
QUrl url(kSpotifyImagesUrl);
QUrlQuery url_query;
url_query.addQueryItem("artist", metadata.artist());
url.setQuery(url_query);
qLog(Debug) << "Fetching artist:" << search_url;
qLog(Debug) << "Fetching artist:" << url;
QNetworkRequest request(search_url);
QNetworkRequest request(url);
QNetworkReply* reply = network_->get(request);
NewClosure(reply, SIGNAL(finished()), [this, id, reply]() {
reply->deleteLater();
QJsonDocument json_document = QJsonDocument::fromJson(reply->readAll());
QJsonObject result = json_document.object();
QJsonObject artists = result["artists"].toObject();
if (artists.isEmpty()) {
QJsonParseError error;
QJsonDocument json_document = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
emit Finished(id);
return;
}
QJsonArray items = artists["items"].toArray();
if (items.isEmpty()) {
emit Finished(id);
return;
}
QJsonObject artist = items.first().toObject();
QString spotify_uri = artist["uri"].toString();
FetchImagesForArtist(id, ExtractSpotifyId(spotify_uri));
});
}
void SpotifyImages::FetchImagesForArtist(int id, const QString& spotify_id) {
QUrl artist_url(QString(kSpotifyArtistUrl).arg(spotify_id));
qLog(Debug) << "Fetching images for artist:" << artist_url;
QNetworkRequest request(artist_url);
QNetworkReply* reply = network_->get(request);
NewClosure(reply, SIGNAL(finished()), [this, id, reply]() {
reply->deleteLater();
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
QJsonObject result = document.object();
QJsonArray images = result["images"].toArray();
QJsonArray results = json_document.array();
QList<QPair<QUrl, QSize>> image_candidates;
for (const QJsonValue& i : images) {
QJsonObject image = i.toObject();
for (const QJsonValue &v : results) {
QJsonObject image = v.toObject();
QUrl url = image["url"].toVariant().toUrl();
int height = image["height"].toInt();
int width = image["width"].toInt();
QUrl url = image["url"].toVariant().toUrl();
image_candidates.append(qMakePair(url, QSize(width, height)));
}
if (!image_candidates.isEmpty()) {
QPair<QUrl, QSize> winner =
*std::max_element(
image_candidates.begin(), image_candidates.end(),
[](const QPair<QUrl, QSize>& a, const QPair<QUrl, QSize>& b) {
return (a.second.height() * a.second.width()) <
(b.second.height() * b.second.width());
});
emit ImageReady(id, winner.first);
QPair<QUrl, QSize> best = *std::max_element(
image_candidates.begin(), image_candidates.end(),
[](const QPair<QUrl, QSize>& a, const QPair<QUrl, QSize>& b) {
return (a.second.height() * a.second.width()) <
(b.second.height() * b.second.width());
});
emit ImageReady(id, best.first);
}
emit Finished(id);
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More