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 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 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 # Emulates the behaviour of codesign --deep which is missing on OS X < 10.9
import os import os

2
dist/copyright.py vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python2
from subprocess import * from subprocess import *
from sys import * from sys import *

2
dist/cpplint.py vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python2
# #
# Copyright (c) 2009 Google Inc. All rights reserved. # Copyright (c) 2009 Google Inc. All rights reserved.
# #

2
dist/format.py vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python2
import argparse import argparse
import difflib import difflib
import os 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. # This file is part of Clementine.
# #

View File

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

View File

@ -1,3 +1,4 @@
#!/usr/bin/python2
import rpm import rpm
import subprocess 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; package pb.remote;
// The supported message types // The supported message types

View File

@ -1197,7 +1197,6 @@ qt5_wrap_cpp(MOC ${HEADERS})
qt5_wrap_ui(UIC ${UI}) qt5_wrap_ui(UIC ${UI})
qt5_add_resources(QRC ${RESOURCES}) qt5_add_resources(QRC ${RESOURCES})
add_pot(POT add_pot(POT
${CMAKE_CURRENT_SOURCE_DIR}/translations/header ${CMAKE_CURRENT_SOURCE_DIR}/translations/header
${CMAKE_CURRENT_SOURCE_DIR}/translations/translations.pot ${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) Analyzer::Base::Base(QWidget* parent, uint scopeSize)
: QWidget(parent), : QWidget(parent),
timeout_(40) // msec timeout_(40), // msec
,
fht_(new FHT(scopeSize)), fht_(new FHT(scopeSize)),
engine_(nullptr), engine_(nullptr),
lastScope_(512), lastScope_(512),
new_frame_(false), new_frame_(false),
is_playing_(false), is_playing_(false),
barkband_table_(QList<uint>()), barkband_table_(),
prev_color_index_(0), prev_color_index_(0),
bands_(0), bands_(0),
psychedelic_enabled_(false) {} psychedelic_enabled_(false) {}
@ -86,15 +85,17 @@ void Analyzer::Base::transform(Scope& scope) {
// values // values
// scope.resize( fht_->size() ); // 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_->logSpectrum(scope.data(), aux.data());
fht_->copy(&f[0], front); fht_->scale(scope.data(), 1.0 / 20);
fht_->logSpectrum(front, &f[0]);
fht_->scale(front, 1.0 / 20);
scope.resize(fht_->size() / 2); // second half of values are rubbish scope.resize(fht_->size() / 2); // second half of values are rubbish
delete[] f;
} }
void Analyzer::Base::paintEvent(QPaintEvent* e) { void Analyzer::Base::paintEvent(QPaintEvent* e) {
@ -202,7 +203,6 @@ void Analyzer::Base::updateBandSize(const int scopeSize) {
bands_ = scopeSize; bands_ = scopeSize;
barkband_table_.clear(); barkband_table_.clear();
barkband_table_.reserve(bands_ + 1);
int barkband = 0; int barkband = 0;
for (int i = 0; i < bands_ + 1; ++i) { 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, QColor Analyzer::Base::getPsychedelicColor(const Scope& scope,
const int ampFactor, const int ampFactor,
const int bias) { const int bias) {
if (scope.size() > barkband_table_.length()) { if (scope.size() > barkband_table_.size()) {
return palette().color(QPalette::Highlight); 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. // Now divide the bark bands into thirds and compute their total amplitudes.
double rgb[3]{}; double rgb[3]{};
for (int i = 0; i < sBarkBandCount - 1; ++i) { 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) { for (int i = 0; i < 3; ++i) {
// bias colours for a threshold around normally amplified audio // bias colours for a threshold around normally amplified audio
rgb[i] = (int)((sqrt(rgb[i]) * ampFactor) + bias); rgb[i] = qMin(255, (int)((sqrt(rgb[i]) * ampFactor) + bias));
if (rgb[i] > 255) {
rgb[i] = 255;
}
} }
return QColor::fromRgb(rgb[0], rgb[1], rgb[2]); return QColor::fromRgb(rgb[0], rgb[1], rgb[2]);

View File

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

View File

@ -100,8 +100,11 @@ void BarAnalyzer::colorChanged() {
rgb = palette().color(QPalette::Highlight); rgb = palette().color(QPalette::Highlight);
} }
for (int x = 0, r = rgb.red(), g = rgb.green(), b = rgb.blue(), r2 = 255 - r; for (int x = 0; x < height(); ++x) {
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) { for (int y = x; y > 0; --y) {
const double fraction = static_cast<double>(y) / height(); const double fraction = static_cast<double>(y) / height();

View File

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

View File

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

View File

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

View File

@ -20,52 +20,40 @@
/* Original Author: Melchior FRANZ <mfranz@kde.org> 2004 /* Original Author: Melchior FRANZ <mfranz@kde.org> 2004
*/ */
#include <math.h> #include <cmath>
#include <string.h>
#include "fht.h" #include "fht.h"
FHT::FHT(int n) : buf_(0), tab_(0), log_(0) { FHT::FHT(int n) : num_((n < 3) ? 0 : 1 << n), exp2_((n < 3) ? -1 : n) {
if (n < 3) {
num_ = 0;
exp2_ = -1;
return;
}
exp2_ = n;
num_ = 1 << n;
if (n > 3) { if (n > 3) {
buf_ = new float[num_]; buf_vector_.resize(num_);
tab_ = new float[num_ * 2]; tab_vector_.resize(num_ * 2);
makeCasTable(); makeCasTable();
} }
} }
FHT::~FHT() { FHT::~FHT() {}
delete[] buf_;
delete[] tab_; int FHT::sizeExp() const { return exp2_; }
delete[] log_; 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) { void FHT::makeCasTable(void) {
float d, *costab, *sintab; float* costab = tab_();
int ul, ndiv2 = num_ / 2; float* sintab = tab_() + num_ / 2 + 1;
for (costab = tab_, sintab = tab_ + num_ / 2 + 1, ul = 0; ul < num_; ul++) { for (int ul = 0; ul < num_; ul++) {
d = M_PI * ul / ndiv2; float d = M_PI * ul / (num_ / 2);
*costab = *sintab = cos(d); *costab = *sintab = cos(d);
costab += 2, sintab += 2; costab += 2;
if (sintab > tab_ + num_ * 2) sintab = tab_ + 1; 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) { void FHT::scale(float* p, float d) {
for (int i = 0; i < (num_ / 2); i++) *p++ *= 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) { void FHT::logSpectrum(float* out, float* p) {
int n = num_ / 2, i, j, k, *r; int n = num_ / 2, i, j, k, *r;
if (!log_) { if (log_vector_.size() < n) {
log_ = new int[n]; log_vector_.resize(n);
float f = n / log10(static_cast<double>(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)); j = static_cast<int>(rint(log10(i + 1.0) * f));
*r = j >= n ? n - 1 : j; *r = j >= n ? n - 1 : j;
} }
} }
semiLogSpectrum(p); semiLogSpectrum(p);
*out++ = *p = *p / 100; *out++ = *p = *p / 100;
for (k = i = 1, r = log_; i < n; i++) { for (k = i = 1, r = log_(); i < n; i++) {
j = *r++; j = *r++;
if (i == j) { if (i == j) {
*out++ = p[i]; *out++ = p[i];
@ -99,10 +87,9 @@ void FHT::logSpectrum(float* out, float* p) {
} }
void FHT::semiLogSpectrum(float* p) { void FHT::semiLogSpectrum(float* p) {
float e;
power2(p); power2(p);
for (int i = 0; i < (num_ / 2); i++, 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; *p = e < 0 ? 0 : e;
} }
} }
@ -110,23 +97,26 @@ void FHT::semiLogSpectrum(float* p) {
void FHT::spectrum(float* p) { void FHT::spectrum(float* p) {
power2(p); power2(p);
for (int i = 0; i < (num_ / 2); i++, 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) { void FHT::power(float* p) {
power2(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) { void FHT::power2(float* p) {
int i;
float* q;
_transform(p, num_, 0); _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) float* q = p + num_ - 2;
*p = (*p * *p) + (*q * *q), p++; 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) { void FHT::transform(float* p) {
@ -172,19 +162,19 @@ void FHT::_transform(float* p, int n, int k) {
int i, j, ndiv2 = n / 2; int i, j, ndiv2 = n / 2;
float a, *t1, *t2, *t3, *t4, *ptab, *pp; 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++; *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);
_transform(p, ndiv2, k + ndiv2); _transform(p, ndiv2, k + ndiv2);
j = num_ / ndiv2 - 1; j = num_ / ndiv2 - 1;
t1 = buf_; t1 = buf_();
t2 = t1 + ndiv2; t2 = t1 + ndiv2;
t3 = p + k + ndiv2; t3 = p + k + ndiv2;
ptab = tab_; ptab = tab_();
pp = p + k; pp = p + k;
a = *ptab++ * *t3++; a = *ptab++ * *t3++;
@ -201,5 +191,6 @@ void FHT::_transform(float* p, int n, int k) {
*t1++ = *pp + a; *t1++ = *pp + a;
*t2++ = *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_ #ifndef ANALYZERS_FHT_H_
#define ANALYZERS_FHT_H_ #define ANALYZERS_FHT_H_
#include <QVector>
/** /**
* Implementation of the Hartley Transform after Bracewell's discrete * Implementation of the Hartley Transform after Bracewell's discrete
* algorithm. The algorithm is subject to US patent No. 4,646,256 (1987) * 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 * [1] Computer in Physics, Vol. 9, No. 4, Jul/Aug 1995 pp 373-379
*/ */
class FHT { class FHT {
int exp2_; const int num_;
int num_; const int exp2_;
float* buf_;
float* tab_; QVector<float> buf_vector_;
int* log_; QVector<float> tab_vector_;
QVector<int> log_vector_;
float* buf_();
float* tab_();
int* log_();
/** /**
* Create a table of "cas" (cosine and sine) values. * 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. * should be at least 3. Values of more than 3 need a trigonometry table.
* @see makeCasTable() * @see makeCasTable()
*/ */
explicit FHT(int); FHT(int);
~FHT(); ~FHT();
inline int sizeExp() const { return exp2_; } int sizeExp() const;
inline int size() const { return num_; } int size() const;
float* copy(float*, float*);
float* clear(float*);
void scale(float*, float); void scale(float*, float);
/** /**

View File

@ -33,12 +33,12 @@
using Analyzer::Scope; using Analyzer::Scope;
const int Rainbow::RainbowAnalyzer::kHeight[] = { 21, 33 }; const int Rainbow::RainbowAnalyzer::kHeight[] = {21, 33};
const int Rainbow::RainbowAnalyzer::kWidth[] = { 34, 53 }; const int Rainbow::RainbowAnalyzer::kWidth[] = {34, 53};
const int Rainbow::RainbowAnalyzer::kFrameCount[] = { 6, 16 }; const int Rainbow::RainbowAnalyzer::kFrameCount[] = {6, 16};
const int Rainbow::RainbowAnalyzer::kRainbowHeight[] = { 21, 16 }; const int Rainbow::RainbowAnalyzer::kRainbowHeight[] = {21, 16};
const int Rainbow::RainbowAnalyzer::kRainbowOverlap[] = { 13, 15 }; const int Rainbow::RainbowAnalyzer::kRainbowOverlap[] = {13, 15};
const int Rainbow::RainbowAnalyzer::kSleepingHeight[] = { 24, 33 }; const int Rainbow::RainbowAnalyzer::kSleepingHeight[] = {24, 33};
const char* Rainbow::NyanCatAnalyzer::kName = "Nyanalyzer Cat"; const char* Rainbow::NyanCatAnalyzer::kName = "Nyanalyzer Cat";
const char* Rainbow::RainbowDashAnalyzer::kName = "Rainbow Dash"; 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::RainbowType Rainbow::RainbowAnalyzer::rainbowtype;
Rainbow::RainbowAnalyzer::RainbowAnalyzer(const RainbowType& rbtype, QWidget* parent) Rainbow::RainbowAnalyzer::RainbowAnalyzer(const RainbowType& rbtype,
QWidget* parent)
: Analyzer::Base(parent, 9), : Analyzer::Base(parent, 9),
timer_id_(startTimer(kFrameIntervalMs)), timer_id_(startTimer(kFrameIntervalMs)),
frame_(0), frame_(0),
@ -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) { void Rainbow::RainbowAnalyzer::timerEvent(QTimerEvent* e) {
if (e->timerId() == timer_id_) { if (e->timerId() == timer_id_) {
@ -88,8 +89,8 @@ void Rainbow::RainbowAnalyzer::resizeEvent(QResizeEvent* e) {
buffer_[0] = QPixmap(); buffer_[0] = QPixmap();
buffer_[1] = QPixmap(); buffer_[1] = QPixmap();
available_rainbow_width_ = width() - kWidth[rainbowtype] available_rainbow_width_ =
+ kRainbowOverlap[rainbowtype]; width() - kWidth[rainbowtype] + kRainbowOverlap[rainbowtype];
px_per_frame_ = px_per_frame_ =
static_cast<float>(available_rainbow_width_) / (kHistorySize - 1) + 1; static_cast<float>(available_rainbow_width_) / (kHistorySize - 1) + 1;
x_offset_ = px_per_frame_ * (kHistorySize - 1) - available_rainbow_width_; x_offset_ = px_per_frame_ * (kHistorySize - 1) - available_rainbow_width_;
@ -129,14 +130,12 @@ void Rainbow::RainbowAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
QPointF* dest = polyline; QPointF* dest = polyline;
float* source = history_; float* source = history_;
const float top_of = const float top_of = static_cast<float>(height()) / 2 -
static_cast<float>(height()) / 2 - static_cast<float>( static_cast<float>(kRainbowHeight[rainbowtype]) / 2;
kRainbowHeight[rainbowtype]) / 2;
for (int band = 0; band < kRainbowBands; ++band) { for (int band = 0; band < kRainbowBands; ++band) {
// Calculate the Y position of this band. // Calculate the Y position of this band.
const float y = const float y = static_cast<float>(kRainbowHeight[rainbowtype]) /
static_cast<float>(kRainbowHeight[rainbowtype]) / ( (kRainbowBands + 1) * (band + 0.5) +
kRainbowBands + 1) * (band + 0.5) +
top_of; top_of;
// Add each point in the line. // Add each point in the line.
@ -205,9 +204,7 @@ void Rainbow::RainbowAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
} }
Rainbow::NyanCatAnalyzer::NyanCatAnalyzer(QWidget* parent) Rainbow::NyanCatAnalyzer::NyanCatAnalyzer(QWidget* parent)
:RainbowAnalyzer(Rainbow::RainbowAnalyzer::Nyancat, parent) { : RainbowAnalyzer(Rainbow::RainbowAnalyzer::Nyancat, parent) {}
}
Rainbow::RainbowDashAnalyzer::RainbowDashAnalyzer(QWidget* 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) { void Sonogram::transform(Scope& scope) {
float* front = static_cast<float*>(&scope.front()); fht_->power2(scope.data());
fht_->power2(front); fht_->scale(scope.data(), 1.0 / 256);
fht_->scale(front, 1.0 / 256);
scope.resize(fht_->size() / 2); scope.resize(fht_->size() / 2);
} }

View File

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

View File

@ -79,6 +79,9 @@ GlobalShortcuts::GlobalShortcuts(QWidget* parent)
SIGNAL(Love())); SIGNAL(Love()));
AddShortcut("ban_last_fm_scrobbling", tr("Ban (Last.fm scrobbling)"), AddShortcut("ban_last_fm_scrobbling", tr("Ban (Last.fm scrobbling)"),
SIGNAL(Ban())); 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"), AddRatingShortcut("rate_zero_star", tr("Rate the current song 0 stars"),
rating_signals_mapper_, 0); rating_signals_mapper_, 0);

View File

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

View File

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

View File

@ -80,6 +80,8 @@ void RegisterMetaTypes() {
"PlaylistSequence::RepeatMode"); "PlaylistSequence::RepeatMode");
qRegisterMetaType<PlaylistSequence::ShuffleMode>( qRegisterMetaType<PlaylistSequence::ShuffleMode>(
"PlaylistSequence::ShuffleMode"); "PlaylistSequence::ShuffleMode");
qRegisterMetaType<QAbstractSocket::SocketState>(
"QAbstractSocket::SocketState");
qRegisterMetaType<QList<PodcastEpisode>>("QList<PodcastEpisode>"); qRegisterMetaType<QList<PodcastEpisode>>("QList<PodcastEpisode>");
qRegisterMetaType<QList<Podcast>>("QList<Podcast>"); qRegisterMetaType<QList<Podcast>>("QList<Podcast>");
qRegisterMetaType<QList<QNetworkCookie>>("QList<QNetworkCookie>"); 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); app_->playlist_manager()->active()->set_current_row(i);
if (i == -1) { if (i == -1) {
Stop(); Stop();
PlayAt(i, change, true);
return; return;
} }

View File

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

View File

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

View File

@ -34,20 +34,23 @@
#include "config.h" #include "config.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/player.h" #include "core/player.h"
#include "core/utilities.h"
#include "core/signalchecker.h" #include "core/signalchecker.h"
#include "core/song.h" #include "core/song.h"
#include "core/tagreaderclient.h" #include "core/tagreaderclient.h"
#include "core/timeconstants.h" #include "core/timeconstants.h"
#include "core/waitforsignal.h"
#include "internet/lastfm/fixlastfm.h" #include "internet/lastfm/fixlastfm.h"
#include "internet/core/internetmodel.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/librarybackend.h"
#include "library/sqlrow.h" #include "library/sqlrow.h"
#include "playlistparsers/cueparser.h" #include "playlistparsers/cueparser.h"
#include "playlistparsers/parserbase.h" #include "playlistparsers/parserbase.h"
#include "playlistparsers/playlistparser.h" #include "playlistparsers/playlistparser.h"
#include "internet/podcasts/podcastparser.h" #include "utilities.h"
#include "internet/podcasts/podcastservice.h"
#include "internet/podcasts/podcasturlloader.h"
#ifdef HAVE_AUDIOCD #ifdef HAVE_AUDIOCD
#include <gst/audio/gstaudiocdsrc.h> #include <gst/audio/gstaudiocdsrc.h>
@ -115,6 +118,11 @@ SongLoader::Result SongLoader::Load(const QUrl& url) {
return Success; return Success;
} }
// It could be a playlist, we give it a shot.
if (LoadRemotePlaylist(url_)) {
return Success;
}
url_ = PodcastUrlLoader::FixPodcastUrl(url_); url_ = PodcastUrlLoader::FixPodcastUrl(url_);
preload_func_ = std::bind(&SongLoader::LoadRemote, this); preload_func_ = std::bind(&SongLoader::LoadRemote, this);
@ -144,10 +152,10 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString& filename) {
SongLoader::Result SongLoader::LoadAudioCD() { SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD #ifdef HAVE_AUDIOCD
CddaSongLoader* cdda_song_loader = new CddaSongLoader; CddaSongLoader* cdda_song_loader = new CddaSongLoader;
connect(cdda_song_loader, SIGNAL(SongsDurationLoaded(SongList)), connect(cdda_song_loader, SIGNAL(SongsDurationLoaded(SongList)), this,
this, SLOT(AudioCDTracksLoadedSlot(SongList))); SLOT(AudioCDTracksLoadedSlot(SongList)));
connect(cdda_song_loader, SIGNAL(SongsMetadataLoaded(SongList)), connect(cdda_song_loader, SIGNAL(SongsMetadataLoaded(SongList)), this,
this, SLOT(AudioCDTracksTagsLoaded(SongList))); SLOT(AudioCDTracksTagsLoaded(SongList)));
cdda_song_loader->LoadSongs(); cdda_song_loader->LoadSongs();
return Success; return Success;
#else // HAVE_AUDIOCD #else // HAVE_AUDIOCD
@ -194,8 +202,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString& filename) {
} }
// It's not in the database, load it asynchronously. // It's not in the database, load it asynchronously.
preload_func_ = preload_func_ = std::bind(&SongLoader::LoadLocalAsync, this, filename);
std::bind(&SongLoader::LoadLocalAsync, this, filename);
return BlockingLoadRequired; return BlockingLoadRequired;
} }
@ -217,8 +224,8 @@ void SongLoader::LoadLocalAsync(const QString& filename) {
if (!parser) { if (!parser) {
// Check the file extension as well, maybe the magic failed, or it was a // 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. // basic M3U file which is just a plain list of filenames.
parser = playlist_parser_-> parser = playlist_parser_->ParserForExtension(
ParserForExtension(QFileInfo(filename).suffix().toLower()); QFileInfo(filename).suffix().toLower());
} }
if (parser) { 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 // 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"); GstPad* pad = gst_element_get_static_pad(fakesink, "sink");
gst_pad_add_probe( gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, &DataReady, this, NULL);
pad, GST_PAD_PROBE_TYPE_BUFFER, &DataReady, this, NULL);
gst_object_unref(pad); gst_object_unref(pad);
QEventLoop loop; QEventLoop loop;
@ -447,12 +453,11 @@ void SongLoader::TypeFound(GstElement*, uint, GstCaps* caps, void* self) {
instance->StopTypefindAsync(true); instance->StopTypefindAsync(true);
} }
GstPadProbeReturn SongLoader::DataReady( GstPadProbeReturn SongLoader::DataReady(GstPad*, GstPadProbeInfo* info,
GstPad*, GstPadProbeInfo* info, gpointer self) { gpointer self) {
SongLoader* instance = reinterpret_cast<SongLoader*>(self); SongLoader* instance = reinterpret_cast<SongLoader*>(self);
if (instance->state_ == Finished) if (instance->state_ == Finished) return GST_PAD_PROBE_OK;
return GST_PAD_PROBE_OK;
GstBuffer* buffer = gst_pad_probe_info_get_buffer(info); GstBuffer* buffer = gst_pad_probe_info_get_buffer(info);
GstMapInfo map; GstMapInfo map;
@ -617,3 +622,61 @@ void SongLoader::StopTypefindAsync(bool success) {
metaObject()->invokeMethod(this, "StopTypefind", Qt::QueuedConnection); 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(); void LoadMetadataBlocking();
Result LoadAudioCD(); Result LoadAudioCD();
signals: signals:
void AudioCDTracksLoaded(); void AudioCDTracksLoaded();
void LoadAudioCDFinished(bool success); void LoadAudioCDFinished(bool success);
void LoadRemoteFinished(); void LoadRemoteFinished();
@ -93,7 +93,12 @@ class SongLoader : public QObject {
#endif // HAVE_AUDIOCD #endif // HAVE_AUDIOCD
private: private:
enum State { WaitingForType, WaitingForMagic, WaitingForData, Finished, }; enum State {
WaitingForType,
WaitingForMagic,
WaitingForData,
Finished,
};
Result LoadLocal(const QString& filename); Result LoadLocal(const QString& filename);
void LoadLocalAsync(const QString& filename); void LoadLocalAsync(const QString& filename);
@ -105,6 +110,7 @@ class SongLoader : public QObject {
void AddAsRawStream(); void AddAsRawStream();
void LoadRemote(); void LoadRemote();
bool LoadRemotePlaylist(const QUrl& url);
// GStreamer callbacks // GStreamer callbacks
static void TypeFound(GstElement* typefind, uint probability, GstCaps* caps, static void TypeFound(GstElement* typefind, uint probability, GstCaps* caps,

View File

@ -100,7 +100,8 @@ QString PrettyTime(int seconds) {
QString ret; QString ret;
if (hours) 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 else
ret.sprintf("%d:%02d", minutes, seconds); // NOLINT(runtime/printf) ret.sprintf("%d:%02d", minutes, seconds); // NOLINT(runtime/printf)
@ -161,11 +162,15 @@ QString PrettySize(quint64 bytes) {
if (bytes <= 1000) if (bytes <= 1000)
ret = QString::number(bytes) + " bytes"; ret = QString::number(bytes) + " bytes";
else if (bytes <= 1000 * 1000) 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) 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 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; return ret;
} }
@ -231,6 +236,23 @@ QString GetTemporaryFileName() {
return file; 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) { bool RemoveRecursive(const QString& path) {
QDir dir(path); QDir dir(path);
for (const QString& child : for (const QString& child :
@ -562,19 +584,13 @@ bool ParseUntilElement(QXmlStreamReader* reader, const QString& name) {
} }
QDateTime ParseRFC822DateTime(const QString& text) { 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) { if (regexp.indexIn(text) == -1) {
return QDateTime(); return QDateTime();
} }
enum class MatchNames { enum class MatchNames { DAYS = 1, MONTHS, YEARS, HOURS, MINUTES, SECONDS };
DAYS = 1,
MONTHS,
YEARS,
HOURS,
MINUTES,
SECONDS
};
QMap<QString, int> monthmap; QMap<QString, int> monthmap;
monthmap["Jan"] = 1; 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::MINUTES)).toInt(),
regexp.cap(static_cast<int>(MatchNames::SECONDS)).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) { 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 MakeTempDir(const QString template_name = QString());
QString GetTemporaryFileName(); QString GetTemporaryFileName();
QString SaveToTemporaryFile(const QByteArray& data);
bool RemoveRecursive(const QString& path); bool RemoveRecursive(const QString& path);
bool CopyRecursive(const QString& source, const QString& destination); bool CopyRecursive(const QString& source, const QString& destination);
bool Copy(QIODevice* source, QIODevice* destination); bool Copy(QIODevice* source, QIODevice* destination);

View File

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

View File

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

View File

@ -24,13 +24,11 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDesktopServices> #include <QDesktopServices>
#include <QMenu> #include <QMenu>
#include <QNetworkRequest>
#include <QNetworkReply> #include <QNetworkReply>
#include <QNetworkRequest>
#include <QXmlStreamReader> #include <QXmlStreamReader>
#include <QtDebug> #include <QtDebug>
#include "intergalacticfmurlhandler.h"
#include "internet/core/internetmodel.h"
#include "core/application.h" #include "core/application.h"
#include "core/closure.h" #include "core/closure.h"
#include "core/logging.h" #include "core/logging.h"
@ -40,6 +38,8 @@
#include "core/utilities.h" #include "core/utilities.h"
#include "globalsearch/globalsearch.h" #include "globalsearch/globalsearch.h"
#include "globalsearch/intergalacticfmsearchprovider.h" #include "globalsearch/intergalacticfmsearchprovider.h"
#include "intergalacticfmurlhandler.h"
#include "internet/core/internetmodel.h"
#include "ui/iconloader.h" #include "ui/iconloader.h"
const int IntergalacticFMServiceBase::kStreamsCacheDurationSecs = const int IntergalacticFMServiceBase::kStreamsCacheDurationSecs =
@ -276,6 +276,6 @@ IntergalacticFMService::IntergalacticFMService(Application* app,
InternetModel* parent) InternetModel* parent)
: IntergalacticFMServiceBase( : IntergalacticFMServiceBase(
app, parent, "Intergalactic FM", app, parent, "Intergalactic FM",
QUrl("https://intergalacticfm.com/channels.xml"), QUrl("https://www.intergalactic.fm/channels.xml"),
QUrl("http://intergalacticfm.com"), QUrl(), QUrl("https://www.intergalactic.fm"), QUrl(),
IconLoader::Load("intergalacticfm", IconLoader::Provider)) {} 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::kSearchDelayMsec = 400;
const int SoundCloudService::kSongSearchLimit = 100; const int SoundCloudService::kSongSearchLimit = 100;
const int SoundCloudService::kSongSimpleSearchLimit = 10; const int SoundCloudService::kSongSimpleSearchLimit = 100;
typedef QPair<QString, QString> Param; typedef QPair<QString, QString> Param;
@ -325,9 +325,8 @@ void SoundCloudService::Search(const QString& text, bool now) {
void SoundCloudService::DoSearch() { void SoundCloudService::DoSearch() {
ClearSearchResults(); ClearSearchResults();
QList<Param> parameters; QList<Param> parameters; parameters << Param("q", pending_search_) << Param("limit", QString::number(kSongSearchLimit));
parameters << Param("q", pending_search_); QNetworkReply* reply = CreateRequest("tracks", parameters);
QNetworkReply* reply = CreateRequest("/tracks", parameters);
const int id = next_pending_search_id_++; const int id = next_pending_search_id_++;
NewClosure(reply, SIGNAL(finished()), this, NewClosure(reply, SIGNAL(finished()), this,
SLOT(SearchFinished(QNetworkReply*, int)), reply, id); SLOT(SearchFinished(QNetworkReply*, int)), reply, id);
@ -355,8 +354,8 @@ void SoundCloudService::ClearSearchResults() {
int SoundCloudService::SimpleSearch(const QString& text) { int SoundCloudService::SimpleSearch(const QString& text) {
QList<Param> parameters; QList<Param> parameters;
parameters << Param("q", text); parameters << Param("q", text) << Param("limit", QString::number(kSongSimpleSearchLimit));
QNetworkReply* reply = CreateRequest("/tracks", parameters); QNetworkReply* reply = CreateRequest("tracks", parameters);
const int id = next_pending_search_id_++; const int id = next_pending_search_id_++;
NewClosure(reply, SIGNAL(finished()), this, NewClosure(reply, SIGNAL(finished()), this,
SLOT(SimpleSearchFinished(QNetworkReply*, int)), reply, id); SLOT(SimpleSearchFinished(QNetworkReply*, int)), reply, id);

View File

@ -395,16 +395,8 @@ void SpotifyService::AddSongsToStarred(const QList<QUrl>& songs_urls) {
server_->AddSongsToStarred(songs_urls); server_->AddSongsToStarred(songs_urls);
} }
void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) { void SpotifyService::InitSearch() {
if (login_task_id_) { search_ = new QStandardItem(IconLoader::Load("edit-find", IconLoader::Base),
app_->task_manager()->SetTaskFinished(login_task_id_);
login_task_id_ = 0;
}
// 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")); tr("Search results"));
search_->setToolTip( search_->setToolTip(
tr("Start typing something on the search box above to " tr("Start typing something on the search box above to "
@ -421,8 +413,8 @@ void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) {
InternetModel::Role_PlayBehaviour); InternetModel::Role_PlayBehaviour);
starred_->setData(true, InternetModel::Role_CanBeModified); starred_->setData(true, InternetModel::Role_CanBeModified);
inbox_ = new QStandardItem(IconLoader::Load("mail-message", inbox_ = new QStandardItem(IconLoader::Load("mail-message", IconLoader::Base),
IconLoader::Base), tr("Inbox")); tr("Inbox"));
inbox_->setData(Type_InboxPlaylist, InternetModel::Role_Type); inbox_->setData(Type_InboxPlaylist, InternetModel::Role_Type);
inbox_->setData(true, InternetModel::Role_CanLazyLoad); inbox_->setData(true, InternetModel::Role_CanLazyLoad);
inbox_->setData(InternetModel::PlayBehaviour_MultipleItems, inbox_->setData(InternetModel::PlayBehaviour_MultipleItems,
@ -438,6 +430,17 @@ void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) {
root_->appendRow(toplist_); root_->appendRow(toplist_);
root_->appendRow(starred_); root_->appendRow(starred_);
root_->appendRow(inbox_); root_->appendRow(inbox_);
}
void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) {
if (login_task_id_) {
app_->task_manager()->SetTaskFinished(login_task_id_);
login_task_id_ = 0;
}
// Create starred and inbox playlists if they're not here already
if (!search_) {
InitSearch();
} else { } else {
// Always reset starred playlist // Always reset starred playlist
// TODO: might be improved by including starred playlist in the response, // TODO: might be improved by including starred playlist in the response,
@ -618,8 +621,8 @@ QList<QAction*> SpotifyService::playlistitem_actions(const Song& song) {
playlistitem_actions_.append(add_to_starred); playlistitem_actions_.append(add_to_starred);
// Create a menu with 'add to playlist' actions for each Spotify playlist // Create a menu with 'add to playlist' actions for each Spotify playlist
QAction* add_to_playlists = new QAction(IconLoader::Load("list-add", QAction* add_to_playlists =
IconLoader::Base), new QAction(IconLoader::Load("list-add", IconLoader::Base),
tr("Add to Spotify playlists"), this); tr("Add to Spotify playlists"), this);
QMenu* playlists_menu = new QMenu(); QMenu* playlists_menu = new QMenu();
for (const QStandardItem* playlist_item : playlists_) { for (const QStandardItem* playlist_item : playlists_) {
@ -678,8 +681,7 @@ void SpotifyService::EnsureMenuCreated() {
song_context_menu_->addSeparator(); song_context_menu_->addSeparator();
remove_from_playlist_ = song_context_menu_->addAction( remove_from_playlist_ = song_context_menu_->addAction(
IconLoader::Load("list-remove", IconLoader::Base), IconLoader::Load("list-remove", IconLoader::Base),
tr("Remove from playlist"), this, tr("Remove from playlist"), this, SLOT(RemoveCurrentFromPlaylist()));
SLOT(RemoveCurrentFromPlaylist()));
song_context_menu_->addAction(tr("Get a URL to share this Spotify song"), song_context_menu_->addAction(tr("Get a URL to share this Spotify song"),
this, SLOT(GetCurrentSongUrlToShare())); this, SLOT(GetCurrentSongUrlToShare()));
song_context_menu_->addSeparator(); song_context_menu_->addSeparator();
@ -765,6 +767,10 @@ void SpotifyService::SearchResults(
ClearSearchResults(); ClearSearchResults();
// Must initialize search pointer if it is nullptr
if (!search_) {
InitSearch();
}
// Fill results list // Fill results list
for (const Song& song : songs) { for (const Song& song : songs) {
QStandardItem* child = CreateSongItem(song); QStandardItem* child = CreateSongItem(song);
@ -848,10 +854,10 @@ void SpotifyService::ItemDoubleClicked(QStandardItem* item) {}
void SpotifyService::DropMimeData(const QMimeData* data, void SpotifyService::DropMimeData(const QMimeData* data,
const QModelIndex& index) { const QModelIndex& index) {
QModelIndex playlist_root_index = index; QModelIndex playlist_root_index = index;
QVariant q_playlist_type = playlist_root_index.data(InternetModel::Role_Type); 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 // In case song was dropped on a playlist item, not on the playlist
// title/root element // title/root element
playlist_root_index = index.parent(); playlist_root_index = index.parent();
@ -864,7 +870,8 @@ void SpotifyService::DropMimeData(const QMimeData* data,
if (playlist_type == Type_StarredPlaylist) { if (playlist_type == Type_StarredPlaylist) {
AddSongsToStarred(data->urls()); AddSongsToStarred(data->urls());
} else if (playlist_type == InternetModel::Type_UserPlaylist) { } 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; if (!q_playlist_index.isValid()) return;
AddSongsToUserPlaylist(q_playlist_index.toInt(), data->urls()); AddSongsToUserPlaylist(q_playlist_index.toInt(), data->urls());
} }
@ -916,8 +923,7 @@ void SpotifyService::SyncPlaylistProgress(
} }
QAction* SpotifyService::GetNewShowConfigAction() { QAction* SpotifyService::GetNewShowConfigAction() {
QAction* action = new QAction(IconLoader::Load("configure", QAction* action = new QAction(IconLoader::Load("configure", IconLoader::Base),
IconLoader::Base),
tr("Configure Spotify..."), this); tr("Configure Spotify..."), this);
connect(action, SIGNAL(triggered()), this, SLOT(ShowConfig())); connect(action, SIGNAL(triggered()), this, SLOT(ShowConfig()));
return action; 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 // the pointer (or adding it to menu or anything else that will take ownership
// of it) // of it)
QAction* GetNewShowConfigAction(); QAction* GetNewShowConfigAction();
void InitSearch();
void ClearSearchResults(); void ClearSearchResults();
QStandardItem* PlaylistBySpotifyIndex(int index) const; QStandardItem* PlaylistBySpotifyIndex(int index) const;
bool DoPlaylistsDiffer(const pb::spotify::Playlists& response) const; bool DoPlaylistsDiffer(const pb::spotify::Playlists& response) const;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -273,9 +273,7 @@ void PlaylistContainer::PlaylistAdded(int id, const QString& name,
} }
} }
void PlaylistContainer::Started() { void PlaylistContainer::Started() { starting_up_ = false; }
starting_up_ = false;
}
void PlaylistContainer::PlaylistClosed(int id) { void PlaylistContainer::PlaylistClosed(int id) {
ui_->tab_bar->RemoveTab(id); ui_->tab_bar->RemoveTab(id);
@ -436,7 +434,6 @@ bool PlaylistContainer::eventFilter(QObject* objectWatched, QEvent* event) {
if (event->type() == QEvent::KeyPress) { if (event->type() == QEvent::KeyPress) {
QKeyEvent* e = static_cast<QKeyEvent*>(event); QKeyEvent* e = static_cast<QKeyEvent*>(event);
switch (e->key()) { switch (e->key()) {
case Qt::Key_Up:
case Qt::Key_Down: case Qt::Key_Down:
case Qt::Key_PageUp: case Qt::Key_PageUp:
case Qt::Key_PageDown: case Qt::Key_PageDown:

View File

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

View File

@ -184,7 +184,7 @@ class PlaylistManager : public PlaylistManagerInterface {
void Load(const QString& filename); void Load(const QString& filename);
void Save(int id, const QString& filename, Playlist::Path path_type); 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 // 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 Rename(int id, const QString& new_name);
void Favorite(int id, bool favorite); void Favorite(int id, bool favorite);
void Delete(int id); void Delete(int id);
@ -225,6 +225,8 @@ class PlaylistManager : public PlaylistManagerInterface {
// Removes items with given indices from the playlist. This operation is not // Removes items with given indices from the playlist. This operation is not
// undoable. // undoable.
void RemoveItemsWithoutUndo(int id, const QList<int>& indices); void RemoveItemsWithoutUndo(int id, const QList<int>& indices);
// Remove the current playing song
void RemoveCurrentSong();
private slots: private slots:
void SetActivePlaying(); void SetActivePlaying();

View File

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

View File

@ -74,7 +74,7 @@ class PlaylistView : public QTreeView {
void SetApplication(Application* app); void SetApplication(Application* app);
void SetItemDelegates(LibraryBackend* backend); void SetItemDelegates(LibraryBackend* backend);
void SetPlaylist(Playlist* playlist); void SetPlaylist(Playlist* playlist);
void RemoveSelected(); void RemoveSelected(bool deleting_from_disk);
void SetReadOnlySettings(bool read_only) { read_only_settings_ = read_only; } void SetReadOnlySettings(bool read_only) { read_only_settings_ = read_only; }

View File

@ -53,6 +53,17 @@ QStringList PlaylistParser::file_extensions() const {
return ret; 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 { QString PlaylistParser::filters() const {
QStringList filters; QStringList filters;
QStringList all_extensions; QStringList all_extensions;
@ -91,6 +102,15 @@ ParserBase* PlaylistParser::ParserForExtension(const QString& suffix) const {
return nullptr; 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, ParserBase* PlaylistParser::ParserForMagic(const QByteArray& data,
const QString& mime_type) const { const QString& mime_type) const {
for (ParserBase* p : parsers_) { for (ParserBase* p : parsers_) {

View File

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

View File

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

View File

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