This commit is contained in:
asiviero 2014-01-08 02:05:35 -02:00
commit aeb67594ba
38 changed files with 475 additions and 83 deletions

View File

@ -158,6 +158,10 @@ include_directories(${Boost_INCLUDE_DIRS})
include_directories(${TAGLIB_INCLUDE_DIRS})
include_directories(${QJSON_INCLUDE_DIRS})
include_directories(${GSTREAMER_INCLUDE_DIRS})
include_directories(${GSTREAMER_APP_INCLUDE_DIRS})
include_directories(${GSTREAMER_BASE_INCLUDE_DIRS})
include_directories(${GSTREAMER_CDDA_INCLUDE_DIRS})
include_directories(${GSTREAMER_TAG_INCLUDE_DIRS})
include_directories(${GLIB_INCLUDE_DIRS})
include_directories(${GLIBCONFIG_INCLUDE_DIRS})
include_directories(${LIBXML_INCLUDE_DIRS})

View File

@ -257,6 +257,7 @@ message ResponseSongFileChunk {
optional SongMetadata song_metadata = 6; // only sent with first chunk!
optional bytes data = 7;
optional int32 size = 8;
optional bytes file_hash = 9;
}
message ResponseLibraryChunk {
@ -264,6 +265,7 @@ message ResponseLibraryChunk {
optional int32 chunk_count = 2;
optional bytes data = 3;
optional int32 size = 4;
optional bytes file_hash = 5;
}
message ResponseSongOffer {
@ -276,7 +278,7 @@ message RequestRateSong {
// The message itself
message Message {
optional int32 version = 1 [default=13];
optional int32 version = 1 [default=14];
optional MsgType type = 2 [default=UNKNOWN]; // What data is in the message?
optional RequestConnect request_connect = 21;

View File

@ -134,6 +134,7 @@ set(SOURCES
devices/deviceproperties.cpp
devices/devicestatefiltermodel.cpp
devices/deviceview.cpp
devices/deviceviewcontainer.cpp
devices/filesystemdevice.cpp
engines/enginebase.cpp
@ -447,6 +448,7 @@ set(HEADERS
devices/deviceproperties.h
devices/devicestatefiltermodel.h
devices/deviceview.h
devices/deviceviewcontainer.h
devices/filesystemdevice.h
engines/enginebase.h
@ -674,6 +676,7 @@ set(UI
covers/coversearchstatisticsdialog.ui
devices/deviceproperties.ui
devices/deviceviewcontainer.ui
globalsearch/globalsearchsettingspage.ui
globalsearch/globalsearchview.ui

View File

@ -442,6 +442,8 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
if (pb.has_rating()) {
d->rating_ = pb.rating();
}
InitArtManual();
}
void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const {
@ -545,6 +547,8 @@ void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) {
d->performer_ = tostr(col + 38);
d->grouping_ = tostr(col + 39);
InitArtManual();
#undef tostr
#undef toint
#undef tolonglong
@ -569,6 +573,17 @@ void Song::InitFromFilePartial(const QString& filename) {
}
}
void Song::InitArtManual() {
// If we don't have an art, check if we have one in the cache
if (d->art_manual_.isEmpty() && d->art_automatic_.isEmpty()) {
QString filename(Utilities::Sha1CoverHash(d->artist_, d->album_).toHex() + ".jpg");
QString path(AlbumCoverLoader::ImageCacheDir() + "/" + filename);
if (QFile::exists(path)) {
d->art_manual_ = path;
}
}
}
#ifdef HAVE_LIBLASTFM
void Song::InitFromLastFM(const lastfm::Track& track) {
d->valid_ = true;

View File

@ -106,6 +106,7 @@ class Song {
void InitFromProtobuf(const pb::tagreader::SongMetadata& pb);
void InitFromQuery(const SqlRow& query, bool reliable_metadata, int col = 0);
void InitFromFilePartial(const QString& filename); // Just store the filename: incomplete but fast
void InitArtManual(); // Check if there is already a art in the cache and store the filename in art_manual
#ifdef HAVE_LIBLASTFM
void InitFromLastFM(const lastfm::Track& track);
#endif

View File

@ -25,6 +25,7 @@
#include <QDateTime>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QIODevice>
#include <QMetaEnum>
#include <QMouseEvent>
@ -455,6 +456,31 @@ QByteArray Sha256(const QByteArray& data) {
return ret;
}
// File must not be open and will be closed afterwards!
QByteArray Md5File(QFile &file) {
file.open(QIODevice::ReadOnly);
QCryptographicHash hash(QCryptographicHash::Md5);
QByteArray data;
while(!file.atEnd()) {
data = file.read(1000000); // 1 mib
hash.addData(data.data(), data.length());
data.clear();
}
file.close();
return hash.result();
}
QByteArray Sha1CoverHash(const QString& artist, const QString& album) {
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(artist.toLower().toUtf8().constData());
hash.addData(album.toLower().toUtf8().constData());
return hash.result();
}
QString PrettySize(const QSize& size) {
return QString::number(size.width()) + "x" +
QString::number(size.height());

View File

@ -19,6 +19,7 @@
#define UTILITIES_H
#include <QColor>
#include <QFile>
#include <QLocale>
#include <QCryptographicHash>
#include <QSize>
@ -66,6 +67,8 @@ namespace Utilities {
QByteArray HmacSha256(const QByteArray& key, const QByteArray& data);
QByteArray HmacSha1(const QByteArray& key, const QByteArray& data);
QByteArray Sha256(const QByteArray& data);
QByteArray Md5File(QFile& file);
QByteArray Sha1CoverHash(const QString& artist, const QString& album);
// Picks an unused ephemeral port number. Doesn't hold the port open so

View File

@ -0,0 +1,56 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.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
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "deviceviewcontainer.h"
#include "ui_deviceviewcontainer.h"
#include "ui/iconloader.h"
DeviceViewContainer::DeviceViewContainer(QWidget *parent)
: QWidget(parent),
ui_(new Ui::DeviceViewContainer),
loaded_icons_(false) {
ui_->setupUi(this);
QPalette palette(ui_->windows_is_broken_frame->palette());
palette.setColor(QPalette::Background, QColor(255, 255, 222));
ui_->windows_is_broken_frame->setPalette(palette);
#ifdef Q_OS_WIN
ui_->windows_is_broken_frame->show();
#else
ui_->windows_is_broken_frame->hide();
#endif
}
DeviceViewContainer::~DeviceViewContainer() {
delete ui_;
}
void DeviceViewContainer::showEvent(QShowEvent* e) {
if (!loaded_icons_) {
loaded_icons_ = true;
ui_->close_frame_button->setIcon(IconLoader::Load("edit-delete"));
ui_->warning_icon->setPixmap(IconLoader::Load("dialog-warning").pixmap(22));
}
QWidget::showEvent(e);
}
DeviceView* DeviceViewContainer::view() const {
return ui_->view;
}

View File

@ -0,0 +1,46 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.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
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DEVICEVIEWCONTAINER_H
#define DEVICEVIEWCONTAINER_H
#include <QWidget>
namespace Ui {
class DeviceViewContainer;
}
class DeviceView;
class DeviceViewContainer : public QWidget {
Q_OBJECT
public:
explicit DeviceViewContainer(QWidget* parent = 0);
~DeviceViewContainer();
DeviceView* view() const;
protected:
void showEvent(QShowEvent*);
private:
Ui::DeviceViewContainer* ui_;
bool loaded_icons_;
};
#endif // DEVICEVIEWCONTAINER_H

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DeviceViewContainer</class>
<widget class="QWidget" name="DeviceViewContainer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>391</width>
<height>396</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="windows_is_broken_frame">
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="warning_icon"/>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>iPods and USB devices currently don't work on Windows. Sorry!</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="close_frame_button"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="DeviceView" name="view" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>DeviceView</class>
<extends>QWidget</extends>
<header>devices/deviceview.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>close_frame_button</sender>
<signal>clicked()</signal>
<receiver>windows_is_broken_frame</receiver>
<slot>hide()</slot>
<hints>
<hint type="sourcelabel">
<x>362</x>
<y>31</y>
</hint>
<hint type="destinationlabel">
<x>369</x>
<y>40</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1679,7 +1679,12 @@ Song GroovesharkService::ExtractSong(const QVariantMap& result_song) {
Song song;
if (!result_song.isEmpty()) {
int song_id = result_song["SongID"].toInt();
QString song_name = result_song["SongName"].toString();
QString song_name;
if (result_song.contains("SongName")) {
song_name = result_song["SongName"].toString();
} else {
song_name = result_song["Name"].toString();
}
int artist_id = result_song["ArtistID"].toInt();
QString artist_name = result_song["ArtistName"].toString();
int album_id = result_song["AlbumID"].toInt();

View File

@ -641,6 +641,11 @@ void OutgoingDataCreator::SendSingleSong(RemoteClient* client, const Song &song,
// Open the file
QFile file(song.url().toLocalFile());
// Get md5 for file
QByteArray md5 = Utilities::Md5File(file).toHex();
qLog(Debug) << "md5 for file" << song.url().toLocalFile() << "=" << md5;
file.open(QIODevice::ReadOnly);
QByteArray data;
@ -665,6 +670,7 @@ void OutgoingDataCreator::SendSingleSong(RemoteClient* client, const Song &song,
chunk->set_file_number(song_no);
chunk->set_size(file.size());
chunk->set_data(data.data(), data.size());
chunk->set_file_hash(md5.data(), md5.size());
// On the first chunk send the metadata, so the client knows
// what file it receives.
@ -746,6 +752,11 @@ void OutgoingDataCreator::SendLibrary(RemoteClient *client) {
// Open the file
QFile file(temp_file_name);
// Get the md5 hash
QByteArray md5 = Utilities::Md5File(file).toHex();
qLog(Debug) << "Library md5" << md5;
file.open(QIODevice::ReadOnly);
QByteArray data;
@ -766,6 +777,7 @@ void OutgoingDataCreator::SendLibrary(RemoteClient *client) {
chunk->set_chunk_number(chunk_number);
chunk->set_size(file.size());
chunk->set_data(data.data(), data.size());
chunk->set_file_hash(md5.data(), md5.size());
// Send data directly to the client
client->SendData(&msg);

View File

@ -101,6 +101,7 @@ PlaylistListContainer::~PlaylistListContainer() {
void PlaylistListContainer::showEvent(QShowEvent* e) {
// Loading icons is expensive so only do it when the view is first opened
if (loaded_icons_) {
QWidget::showEvent(e);
return;
}
loaded_icons_ = true;

View File

@ -123,6 +123,7 @@ return_song:
void ASXParser::Save(const SongList& songs, QIODevice* device, const QDir&) const {
QXmlStreamWriter writer(device);
writer.setAutoFormatting(true);
writer.setAutoFormattingIndent(2);
writer.writeStartDocument();
{
StreamElement asx("asx", &writer);

View File

@ -81,6 +81,8 @@ void WplParser::ParseSeq(const QDir& dir, QXmlStreamReader* reader,
void WplParser::Save(const SongList& songs, QIODevice* device,
const QDir& dir) const {
QXmlStreamWriter writer(device);
writer.setAutoFormatting(true);
writer.setAutoFormattingIndent(2);
writer.writeProcessingInstruction("wpl", "version=\"1.0\"");
StreamElement smil("smil", &writer);

View File

@ -104,6 +104,8 @@ return_song:
void XSPFParser::Save(const SongList& songs, QIODevice* device, const QDir&) const {
QXmlStreamWriter writer(device);
writer.setAutoFormatting(true);
writer.setAutoFormattingIndent(2);
writer.writeStartDocument();
StreamElement playlist("playlist", &writer);
writer.writeAttribute("version", "1");

View File

@ -156,8 +156,8 @@ void PodcastEpisode::BindToQuery(QSqlQuery* query) const {
Song PodcastEpisode::ToSong(const Podcast& podcast) const {
Song ret;
ret.set_valid(true);
ret.set_title(title());
ret.set_artist(author());
ret.set_title(title().simplified());
ret.set_artist(author().simplified());
ret.set_length_nanosec(kNsecPerSec * duration_secs());
ret.set_year(publication_date().date().year());
ret.set_comment(description());
@ -172,7 +172,7 @@ Song PodcastEpisode::ToSong(const Podcast& podcast) const {
// Use information from the podcast if it's set
if (podcast.is_valid()) {
ret.set_album(podcast.title());
ret.set_album(podcast.title().simplified());
ret.set_art_automatic(podcast.ImageUrlLarge().toString());
}

View File

@ -139,7 +139,7 @@ void PodcastService::PopulatePodcastList(QStandardItem* parent) {
void PodcastService::UpdatePodcastText(QStandardItem* item, int unlistened_count) const {
const Podcast podcast = item->data(Role_Podcast).value<Podcast>();
QString title = podcast.title();
QString title = podcast.title().simplified();
QFont font;
if (unlistened_count > 0) {
@ -159,7 +159,7 @@ void PodcastService::UpdateEpisodeText(QStandardItem* item,
int percent) {
const PodcastEpisode episode = item->data(Role_Episode).value<PodcastEpisode>();
QString title = episode.title();
QString title = episode.title().simplified();
QString tooltip;
QFont font;
QIcon icon;
@ -237,7 +237,7 @@ QStandardItem* PodcastService::CreatePodcastItem(const Podcast& podcast) {
QStandardItem* PodcastService::CreatePodcastEpisodeItem(const PodcastEpisode& episode) {
QStandardItem* item = new QStandardItem;
item->setText(episode.title());
item->setText(episode.title().simplified());
item->setData(Type_Episode, InternetModel::Role_Type);
item->setData(QVariant::fromValue(episode), Role_Episode);
item->setData(InternetModel::PlayBehaviour_UseSongLoader, InternetModel::Role_PlayBehaviour);

View File

@ -48,13 +48,19 @@ bool ArtistInfoView::NeedsUpdate(const Song& old_metadata, const Song& new_metad
return old_metadata.artist() != new_metadata.artist();
}
void ArtistInfoView::InfoResultReady (int id, const CollapsibleInfoPane::Data& data) {
if (id != current_request_id_)
return;
AddSection (new CollapsibleInfoPane(data, this));
CollapseSections();
}
void ArtistInfoView::ResultReady(int id, const SongInfoFetcher::Result& result) {
if (id != current_request_id_)
return;
Clear();
if (!result.images_.isEmpty()) {
// Image view goes at the top
PrettyImageView* image_view = new PrettyImageView(network_, this);
@ -64,10 +70,6 @@ void ArtistInfoView::ResultReady(int id, const SongInfoFetcher::Result& result)
image_view->AddImage(url);
}
}
foreach (const CollapsibleInfoPane::Data& data, result.info_) {
AddSection(new CollapsibleInfoPane(data, this));
}
CollapseSections();
}

View File

@ -36,6 +36,7 @@ public:
~ArtistInfoView();
protected:
virtual void InfoResultReady (int id, const CollapsibleInfoPane::Data& data);
bool NeedsUpdate(const Song& old_metadata, const Song& new_metadata) const;
protected slots:
@ -43,3 +44,4 @@ protected slots:
};
#endif // ARTISTINFOVIEW_H

View File

@ -89,18 +89,24 @@ void EchoNestBiographies::RequestFinished() {
data.icon_ = site_icons_[canonical_site];
SongInfoTextView* editor = new SongInfoTextView;
QString text;
// Add a link to the bio webpage at the top if we have one
if (!bio.url().isEmpty()) {
text += "<p><a href=\"" + bio.url().toEncoded() + "\">" +
tr("Open in your browser") +
"</a></p>";
}
text += bio.text();
if (bio.site() == "last.fm") {
// Echonest lost formatting and it seems there is currently no plans on Echonest side for changing this.
// But with last.fm, we can guess newlines: " " corresponds to a newline
// (this seems to be because on last.fm' website, extra blank is inserted
// before <br /> tag, and this blank is kept).
// This is tricky, but this make the display nicer for last.fm biographies.
QString copy(bio.text());
copy.replace(" ","<p>");
editor->SetHtml(copy);
} else {
editor->SetHtml(bio.text());
text.replace(" ","<p>");
}
editor->SetHtml(text);
data.contents_ = editor;
emit InfoReady(request->id_, data);

View File

@ -64,6 +64,8 @@ SongInfoBase::SongInfoBase(QWidget* parent)
connect(fetcher_, SIGNAL(ResultReady(int,SongInfoFetcher::Result)),
SLOT(ResultReady(int,SongInfoFetcher::Result)));
connect(fetcher_, SIGNAL(InfoResultReady(int,CollapsibleInfoPane::Data)),
SLOT(InfoResultReady(int,CollapsibleInfoPane::Data)));
}
void SongInfoBase::Clear() {
@ -142,9 +144,13 @@ void SongInfoBase::Update(const Song& metadata) {
// Do this after the new pane has been shown otherwise it'll just grab a
// black rectangle.
Clear ();
QTimer::singleShot(0, fader_, SLOT(StartBlur()));
}
void SongInfoBase::InfoResultReady (int id, const CollapsibleInfoPane::Data& data) {
}
void SongInfoBase::ResultReady(int id, const SongInfoFetcher::Result& result) {
foreach (const CollapsibleInfoPane::Data& data, result.info_) {
delete data.contents_;
@ -208,10 +214,6 @@ void SongInfoBase::ReloadSettings() {
QMetaObject::invokeMethod(contents, "ReloadSettings");
}
QSettings s;
s.beginGroup(kSettingsGroup);
fetcher_->set_timeout(s.value("timeout", SongInfoFetcher::kDefaultTimeoutDuration).toInt());
}
void SongInfoBase::ConnectWidget(QWidget* widget) {
@ -225,3 +227,4 @@ void SongInfoBase::ConnectWidget(QWidget* widget) {
connect(widget, SIGNAL(DoGlobalSearch(QString)), SIGNAL(DoGlobalSearch(QString)));
}
}

View File

@ -63,6 +63,7 @@ protected:
void CollapseSections();
protected slots:
virtual void InfoResultReady (int id, const CollapsibleInfoPane::Data& data);
virtual void ResultReady(int id, const SongInfoFetcher::Result& result);
protected:
@ -94,3 +95,4 @@ private:
};
#endif // SONGINFOBASE_H

View File

@ -68,6 +68,10 @@ void SongInfoFetcher::InfoReady(int id, const CollapsibleInfoPane::Data& data) {
if (!results_.contains(id))
return;
results_[id].info_ << data;
if (!waiting_for_.contains(id))
return;
emit InfoResultReady (id, data);
}
void SongInfoFetcher::ProviderFinished(int id) {
@ -107,3 +111,4 @@ void SongInfoFetcher::Timeout(int id) {
// Remove the timer
delete timeout_timers_.take(id);
}

View File

@ -40,9 +40,7 @@ public:
QList<CollapsibleInfoPane::Data> info_;
};
static const int kDefaultTimeoutDuration = 2500; // msec
void set_timeout(int msec) { timeout_duration_ = msec; }
static const int kDefaultTimeoutDuration = 25000; // msec
void AddProvider(SongInfoProvider* provider);
int FetchInfo(const Song& metadata);
@ -50,6 +48,7 @@ public:
QList<SongInfoProvider*> providers() const { return providers_; }
signals:
void InfoResultReady (int id, const CollapsibleInfoPane::Data& data);
void ResultReady(int id, const SongInfoFetcher::Result& result);
private slots:
@ -72,3 +71,4 @@ private:
};
#endif // SONGINFOFETCHER_H

View File

@ -59,8 +59,6 @@ void SongInfoSettingsPage::Load() {
s.beginGroup(SongInfoTextView::kSettingsGroup);
ui_->song_info_font_size->setValue(
s.value("font_size", SongInfoTextView::kDefaultFontSize).toReal());
ui_->song_info_timeout->setValue(
s.value("timeout", SongInfoFetcher::kDefaultTimeoutDuration).toInt());
s.endGroup();
QList<const UltimateLyricsProvider*> providers = dialog()->song_info_view()->lyric_providers();
@ -80,7 +78,6 @@ void SongInfoSettingsPage::Save() {
s.beginGroup(SongInfoTextView::kSettingsGroup);
s.setValue("font_size", ui_->song_info_font_preview->font().pointSizeF());
s.setValue("timeout", ui_->song_info_timeout->value());
s.endGroup();
s.beginGroup(SongInfoView::kSettingsGroup);

View File

@ -75,38 +75,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Network</string>
</property>
<layout class="QFormLayout" name="formLayout_6">
<item row="0" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Timeout</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="song_info_timeout">
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>1000</number>
</property>
<property name="maximum">
<number>60000</number>
</property>
<property name="value">
<number>2500</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_6">
<property name="sizePolicy">

View File

@ -76,19 +76,17 @@ bool SongInfoView::NeedsUpdate(const Song& old_metadata, const Song& new_metadat
old_metadata.artist() != new_metadata.artist();
}
void SongInfoView::ResultReady(int id, const SongInfoFetcher::Result& result) {
void SongInfoView::InfoResultReady (int id, const CollapsibleInfoPane::Data& data) {
if (id != current_request_id_)
return;
Clear();
foreach (const CollapsibleInfoPane::Data& data, result.info_) {
AddSection(new CollapsibleInfoPane(data, this));
}
AddSection (new CollapsibleInfoPane(data, this));
CollapseSections();
}
void SongInfoView::ResultReady(int id, const SongInfoFetcher::Result& result) {
}
void SongInfoView::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
@ -170,3 +168,4 @@ QList<const UltimateLyricsProvider*> SongInfoView::lyric_providers() const {
qSort(ret.begin(), ret.end(), CompareLyricProviders);
return ret;
}

View File

@ -43,7 +43,8 @@ protected:
bool NeedsUpdate(const Song& old_metadata, const Song& new_metadata) const;
protected slots:
void ResultReady(int id, const SongInfoFetcher::Result& result);
virtual void InfoResultReady (int id, const CollapsibleInfoPane::Data& data);
virtual void ResultReady(int id, const SongInfoFetcher::Result& result);
private:
SongInfoProvider* ProviderByName(const QString& name) const;
@ -56,3 +57,4 @@ private:
};
#endif // SONGINFOVIEW_H

View File

@ -141,7 +141,7 @@ void UltimateLyricsProvider::LyricsFetched() {
ApplyExcludeRule(rule, &lyrics);
}
if (!content.isEmpty()) {
if (!content.isEmpty() and HTMLHasAlphaNumeric(content)) {
lyrics = content;
break;
}
@ -150,7 +150,7 @@ void UltimateLyricsProvider::LyricsFetched() {
lyrics = original_content;
}
if (!lyrics.isEmpty()) {
if (!lyrics.isEmpty() and HTMLHasAlphaNumeric(lyrics)) {
CollapsibleInfoPane::Data data;
data.id_ = "ultimatelyrics/" + name_;
data.title_ = tr("Lyrics from %1").arg(name_);
@ -317,3 +317,19 @@ QString UltimateLyricsProvider::NoSpace(const QString& text) {
ret.remove(' ');
return ret;
}
// tells whether a html block has alphanumeric characters (skipping tags)
// TODO: handle special characters (e.g. &reg; &aacute;)
bool UltimateLyricsProvider::HTMLHasAlphaNumeric(const QString& html) {
bool in_tag = false;
foreach (const QChar& c, html) {
if (!in_tag and c.isLetterOrNumber())
return true;
else if (c == QChar('<'))
in_tag = true;
else if (c == QChar('>'))
in_tag = false;
}
qLog(Debug) << html;
return false;
}

View File

@ -73,6 +73,7 @@ private:
static QString FirstChar(const QString& text);
static QString TitleCase(const QString& text);
static QString NoSpace(const QString& text);
static bool HTMLHasAlphaNumeric(const QString& html);
void ReplaceField(const QString& tag, const QString& value, QString* text) const;
void ReplaceFields(const Song& metadata, QString* text) const;

View File

@ -18,6 +18,7 @@
#include "core/application.h"
#include "core/logging.h"
#include "core/utilities.h"
#include "covers/albumcoverfetcher.h"
#include "covers/albumcoverloader.h"
#include "covers/currentartloader.h"
@ -29,7 +30,6 @@
#include "ui/iconloader.h"
#include <QAction>
#include <QCryptographicHash>
#include <QDialog>
#include <QDragEnterEvent>
#include <QFileDialog>
@ -63,6 +63,10 @@ AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget* parent)
unset_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Unset cover"), this);
show_cover_ = new QAction(IconLoader::Load("zoom-in"), tr("Show fullsize..."), this);
search_cover_auto_ = new QAction(IconLoader::Load("find"), tr("Search automatically"), this);
search_cover_auto_->setCheckable(true);
search_cover_auto_->setChecked(false);
separator_ = new QAction(this);
separator_->setSeparator(true);
}
@ -77,6 +81,9 @@ void AlbumCoverChoiceController::SetApplication(Application* app) {
cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), this);
cover_searcher_ = new AlbumCoverSearcher(QIcon(":/nocover.png"), app, this);
cover_searcher_->Init(cover_fetcher_);
connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(quint64,QImage,CoverSearchStatistics)),
this, SLOT(AlbumCoverFetched(quint64,QImage,CoverSearchStatistics)));
}
QList<QAction*> AlbumCoverChoiceController::GetAllActions() {
@ -204,6 +211,27 @@ void AlbumCoverChoiceController::ShowCover(const Song& song) {
dialog->show();
}
void AlbumCoverChoiceController::SearchCoverAutomatically(const Song& song) {
qint64 id = cover_fetcher_->FetchAlbumCover(song.artist(), song.album());
cover_fetching_tasks_[id] = song;
}
void AlbumCoverChoiceController::AlbumCoverFetched(quint64 id,
const QImage& image,
const CoverSearchStatistics& statistics) {
Song song;
if (cover_fetching_tasks_.contains(id)) {
song = cover_fetching_tasks_.take(id);
}
if (!image.isNull()) {
QString cover = SaveCoverInCache(song.artist(), song.album(), image);
SaveCover(&song, cover);
}
emit AutomaticCoverSearchDone();
}
void AlbumCoverChoiceController::SaveCover(Song* song, const QString &cover) {
if(song->is_valid() && song->id() != -1) {
song->set_art_manual(cover);
@ -219,11 +247,7 @@ QString AlbumCoverChoiceController::SaveCoverInCache(
const QString& artist, const QString& album, const QImage& image) {
// Hash the artist and album into a filename for the image
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(artist.toLower().toUtf8().constData());
hash.addData(album.toLower().toUtf8().constData());
QString filename(hash.result().toHex() + ".jpg");
QString filename(Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg");
QString path(AlbumCoverLoader::ImageCacheDir() + "/" + filename);
// Make sure this directory exists first

View File

@ -27,6 +27,7 @@ class AlbumCoverFetcher;
class AlbumCoverSearcher;
class Application;
class CoverFromURLDialog;
class CoverSearchStatistics;
class QFileDialog;
class Song;
@ -52,6 +53,7 @@ class AlbumCoverChoiceController : public QWidget {
QAction* search_for_cover_action() const { return search_for_cover_; }
QAction* unset_cover_action() const { return unset_cover_; }
QAction* show_cover_action() const { return show_cover_; }
QAction* search_cover_auto_action() const { return search_cover_auto_; }
// Returns QAction* for every operation implemented by this controller.
// The list contains QAction* for:
@ -91,6 +93,9 @@ class AlbumCoverChoiceController : public QWidget {
// Shows the cover of given song in it's original size.
void ShowCover(const Song& song);
// Search for covers automatically
void SearchCoverAutomatically(const Song& song);
// Saves the chosen cover as manual cover path of this song in library.
void SaveCover(Song* song, const QString& cover);
@ -103,6 +108,13 @@ class AlbumCoverChoiceController : public QWidget {
static bool CanAcceptDrag(const QDragEnterEvent* e);
signals:
void AutomaticCoverSearchDone();
private slots:
void AlbumCoverFetched(quint64 id, const QImage& image,
const CoverSearchStatistics& statistics);
private:
QString GetInitialPathForFileDialog(const Song& song,
const QString& filename);
@ -124,6 +136,9 @@ private:
QAction* search_for_cover_;
QAction* unset_cover_;
QAction* show_cover_;
QAction* search_cover_auto_;
QMap<quint64, Song> cover_fetching_tasks_;
};
#endif // ALBUMCOVERCHOICECONTROLLER_H

View File

@ -115,6 +115,8 @@ void Equalizer::LoadDefaultPresets() {
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Classical"), Params(0, 0, 0, 0, 0, 0, -40, -40, -40, -50));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Club"), Params(0, 0, 20, 30, 30, 30, 20, 0, 0, 0));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Dance"), Params(50, 35, 10, 0, 0, -30, -40, -40, 0, 0));
// Dubstep equalizer created by Devyn Collier Johnson
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Dubstep"), Params(0, 36, 85, 58, 30, 0, 36, 60, 96, 62, 0));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Full Bass"), Params(70, 70, 70, 40, 20, -45, -50, -55, -55, -55));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Full Treble"), Params(-50, -50, -50, -25, 15, 55, 80, 80, 80, 85));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Full Bass + Treble"), Params(35, 30, 0, -40, -25, 10, 45, 55, 60, 60));
@ -123,6 +125,8 @@ void Equalizer::LoadDefaultPresets() {
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Live"), Params(-25, 0, 20, 25, 30, 30, 20, 15, 15, 10));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Party"), Params(35, 35, 0, 0, 0, 0, 0, 0, 35, 35));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Pop"), Params(-10, 25, 35, 40, 25, -5, -15, -15, -10, -10));
// Psychedelic equalizer created by Devyn Collier Johnson
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Psychedelic"), Params(100, 100, 0, 40, 0, 67, 79, 0, 30, -100, 37));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Reggae"), Params(0, 0, -5, -30, 0, -35, -35, 0, 0, 0));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Rock"), Params(40, 25, -30, -40, -20, 20, 45, 55, 55, 55));
AddPreset(QT_TRANSLATE_NOOP("Equalizer", "Soft"), Params(25, 10, -5, -15, -5, 20, 45, 50, 55, 60));

View File

@ -41,6 +41,7 @@
#include "devices/devicemanager.h"
#include "devices/devicestatefiltermodel.h"
#include "devices/deviceview.h"
#include "devices/deviceviewcontainer.h"
#include "engines/enginebase.h"
#include "engines/gstengine.h"
#include "globalsearch/globalsearch.h"
@ -173,7 +174,8 @@ MainWindow::MainWindow(Application* app,
file_view_(new FileView(this)),
playlist_list_(new PlaylistListContainer(this)),
internet_view_(new InternetViewContainer(this)),
device_view_(new DeviceView(this)),
device_view_container_(new DeviceViewContainer(this)),
device_view_(device_view_container_->view()),
song_info_view_(new SongInfoView(this)),
artist_info_view_(new ArtistInfoView(this)),
settings_dialog_(NULL),
@ -238,7 +240,7 @@ MainWindow::MainWindow(Application* app,
ui_->tabs->AddTab(file_view_, IconLoader::Load("document-open"), tr("Files"));
ui_->tabs->AddTab(playlist_list_, IconLoader::Load("view-media-playlist"), tr("Playlists"));
ui_->tabs->AddTab(internet_view_, IconLoader::Load("applications-internet"), tr("Internet"));
ui_->tabs->AddTab(device_view_, IconLoader::Load("multimedia-player-ipod-mini-blue"), tr("Devices"));
ui_->tabs->AddTab(device_view_container_, IconLoader::Load("multimedia-player-ipod-mini-blue"), tr("Devices"));
ui_->tabs->AddSpacer();
ui_->tabs->AddTab(song_info_view_, IconLoader::Load("view-media-lyrics"), tr("Song info"));
ui_->tabs->AddTab(artist_info_view_, IconLoader::Load("x-clementine-artist"), tr("Artist info"));

View File

@ -44,6 +44,7 @@ class CoverProviders;
class Database;
class DeviceManager;
class DeviceView;
class DeviceViewContainer;
class EditTagDialog;
class Equalizer;
class ErrorDialog;
@ -293,6 +294,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
boost::scoped_ptr<RipCD> rip_cd_;
PlaylistListContainer* playlist_list_;
InternetViewContainer* internet_view_;
DeviceViewContainer* device_view_container_;
DeviceView* device_view_;
SongInfoView* song_info_view_;
ArtistInfoView* artist_info_view_;

View File

@ -72,6 +72,7 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
details_(new QTextDocument(this)),
previous_track_opacity_(0.0),
bask_in_his_glory_action_(NULL),
downloading_covers_(false),
aww_(false),
kittens_(NULL),
pending_kitten_(0)
@ -80,6 +81,7 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
QSettings s;
s.beginGroup(kSettingsGroup);
mode_ = Mode(s.value("mode", SmallSongDetails).toInt());
album_cover_choice_controller_->search_cover_auto_action()->setChecked(s.value("search_for_cover_auto", false).toBool());
// Accept drops for setting album art
setAcceptDrops(true);
@ -96,6 +98,9 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
QList<QAction*> actions = album_cover_choice_controller_->GetAllActions();
// Here we add the search automatically action, too!
actions.append(album_cover_choice_controller_->search_cover_auto_action());
connect(album_cover_choice_controller_->cover_from_file_action(),
SIGNAL(triggered()), this, SLOT(LoadCoverFromFile()));
connect(album_cover_choice_controller_->cover_to_file_action(),
@ -108,6 +113,8 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
SIGNAL(triggered()), this, SLOT(UnsetCover()));
connect(album_cover_choice_controller_->show_cover_action(),
SIGNAL(triggered()), this, SLOT(ShowCover()));
connect(album_cover_choice_controller_->search_cover_auto_action(),
SIGNAL(triggered()), this, SLOT(SearchCoverAutomatically()));
menu_->addActions(actions);
menu_->addSeparator();
@ -129,6 +136,9 @@ NowPlayingWidget::NowPlayingWidget(QWidget* parent)
fade_animation_->setDirection(QTimeLine::Backward); // 1.0 -> 0.0
UpdateHeight();
connect(album_cover_choice_controller_, SIGNAL(AutomaticCoverSearchDone()),
this, SLOT(AutomaticCoverSearchDone()));
}
NowPlayingWidget::~NowPlayingWidget() {
@ -233,9 +243,10 @@ void NowPlayingWidget::KittenLoaded(quint64 id, const QImage& image) {
}
}
void NowPlayingWidget::AlbumArtLoaded(const Song& metadata, const QString&,
void NowPlayingWidget::AlbumArtLoaded(const Song& metadata, const QString& uri,
const QImage& image) {
metadata_ = metadata;
downloading_covers_ = false;
if (aww_) {
pending_kitten_ = kittens_->LoadKitten(app_->current_art_loader()->options());
@ -243,6 +254,9 @@ void NowPlayingWidget::AlbumArtLoaded(const Song& metadata, const QString&,
}
SetImage(image);
// Search for cover automatically?
GetCoverAutomatically();
}
void NowPlayingWidget::SetImage(const QImage& image) {
@ -301,6 +315,9 @@ void NowPlayingWidget::DrawContents(QPainter *p) {
} else {
// Draw the cover
p->drawPixmap(0, 0, small_ideal_height_, small_ideal_height_, cover_);
if (downloading_covers_) {
p->drawPixmap(small_ideal_height_ - 18, 6, 16, 16, spinner_animation_->currentPixmap());
}
}
// Draw the details
@ -321,6 +338,9 @@ void NowPlayingWidget::DrawContents(QPainter *p) {
p->drawPixmap(x_offset, kTopBorder, total_size, total_size, hypnotoad_->currentPixmap());
} else {
p->drawPixmap(x_offset, kTopBorder, total_size, total_size, cover_);
if (downloading_covers_) {
p->drawPixmap(total_size - 31, 40, 16, 16, spinner_animation_->currentPixmap());
}
}
// Work out how high the text is going to be
@ -454,6 +474,15 @@ void NowPlayingWidget::ShowCover() {
album_cover_choice_controller_->ShowCover(metadata_);
}
void NowPlayingWidget::SearchCoverAutomatically() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("search_for_cover_auto", album_cover_choice_controller_->search_cover_auto_action()->isChecked());
// Search for cover automatically?
GetCoverAutomatically();
}
void NowPlayingWidget::Bask() {
big_hypnotoad_.reset(new FullscreenHypnotoad);
big_hypnotoad_->showFullScreen();
@ -472,3 +501,31 @@ void NowPlayingWidget::dropEvent(QDropEvent* e) {
QWidget::dropEvent(e);
}
bool NowPlayingWidget::GetCoverAutomatically() {
// Search for cover automatically?
bool search = album_cover_choice_controller_->search_cover_auto_action()->isChecked() &&
!metadata_.has_manually_unset_cover() &&
metadata_.art_automatic().isEmpty() &&
metadata_.art_manual().isEmpty();
if (search) {
qLog(Debug) << "GetCoverAutomatically";
downloading_covers_ = true;
album_cover_choice_controller_->SearchCoverAutomatically(metadata_);
// Show a spinner animation
spinner_animation_.reset(new QMovie(":/spinner.gif", QByteArray(), this));
connect(spinner_animation_.get(), SIGNAL(updated(const QRect&)), SLOT(update()));
spinner_animation_->start();
update();
}
return search;
}
void NowPlayingWidget::AutomaticCoverSearchDone() {
downloading_covers_ = false;
spinner_animation_.reset();
update();
}

View File

@ -99,9 +99,12 @@ private slots:
void SearchForCover();
void UnsetCover();
void ShowCover();
void SearchCoverAutomatically();
void Bask();
void AutomaticCoverSearchDone();
private:
void CreateModeAction(Mode mode, const QString& text, QActionGroup* group,
QSignalMapper* mapper);
@ -110,6 +113,7 @@ private:
void DrawContents(QPainter* p);
void SetImage(const QImage& image);
void ScaleCover();
bool GetCoverAutomatically();
private:
Application* app_;
@ -144,6 +148,9 @@ private:
boost::scoped_ptr<QMovie> hypnotoad_;
boost::scoped_ptr<FullscreenHypnotoad> big_hypnotoad_;
boost::scoped_ptr<QMovie> spinner_animation_;
bool downloading_covers_;
bool aww_;
KittenLoader* kittens_;
quint64 pending_kitten_;