SomaFM streams

This commit is contained in:
David Sansome 2010-01-18 02:23:55 +00:00
parent 3fcf8233b9
commit 4777b3eab1
21 changed files with 296 additions and 118 deletions

3
TODO
View File

@ -10,12 +10,15 @@
- osd
- Save and load playlists from files
- Shuffle and repeat playlist
- Autocompletion from library when editing tags
- Edit tags in playlist view
- Disabled fields in tag editor
Long-term:
- iPod
- Automatic tagging from musicbrainz
- Album cover fetching from last.fm
Windows:
- Playlist delegates

View File

@ -14,6 +14,7 @@
const char* LastFMService::kServiceName = "Last.fm";
const char* LastFMService::kSettingsGroup = "Last.fm";
const char* LastFMService::kLoadingText = "Loading Last.fm radio";
const char* LastFMService::kAudioscrobblerClientId = "tng";
const char* LastFMService::kApiKey = "75d20fb472be99275392aefa2760ea09";
const char* LastFMService::kSecret = "d3072b60ae626be12be69448f5c46e70";
@ -80,7 +81,7 @@ void LastFMService::ScrobblingEnabledChangedSlot(bool value) {
}
RadioItem* LastFMService::CreateRootItem(RadioItem* parent) {
RadioItem* item = new RadioItem(this, RadioItem::Type_Service, "Last.fm", parent);
RadioItem* item = new RadioItem(this, RadioItem::Type_Service, kServiceName, parent);
item->icon = QIcon(":last.fm/as.png");
return item;
}
@ -252,7 +253,7 @@ void LastFMService::StartLoading(const QUrl& url) {
if (!IsAuthenticated())
return;
emit LoadingStarted();
emit TaskStarted(kLoadingText);
delete tuner_;
@ -284,7 +285,7 @@ void LastFMService::TunerError(lastfm::ws::Error error) {
if (!initial_tune_)
return;
emit LoadingFinished();
emit TaskFinished(kLoadingText);
if (error == lastfm::ws::NotEnoughContent) {
emit StreamFinished();
@ -325,7 +326,7 @@ QString LastFMService::ErrorString(lastfm::ws::Error error) const {
void LastFMService::TunerTrackAvailable() {
if (initial_tune_) {
emit LoadingFinished();
emit TaskFinished(kLoadingText);
LoadNext(last_url_);
initial_tune_ = false;

View File

@ -21,6 +21,7 @@ class LastFMService : public RadioService {
static const char* kServiceName;
static const char* kSettingsGroup;
static const char* kLoadingText;
static const char* kAudioscrobblerClientId;
static const char* kApiKey;
static const char* kSecret;

View File

@ -183,8 +183,8 @@ MainWindow::MainWindow(QWidget *parent)
playlist_menu_->addAction(ui_.action_clear_playlist);
// Radio connections
connect(radio_model_, SIGNAL(LoadingStarted()), ui_.playlist, SLOT(StartRadioLoading()));
connect(radio_model_, SIGNAL(LoadingFinished()), ui_.playlist, SLOT(StopRadioLoading()));
connect(radio_model_, SIGNAL(TaskStarted(QString)), multi_loading_indicator_, SLOT(TaskStarted(QString)));
connect(radio_model_, SIGNAL(TaskFinished(QString)), multi_loading_indicator_, SLOT(TaskFinished(QString)));
connect(radio_model_, SIGNAL(StreamError(QString)), SLOT(ReportError(QString)));
connect(radio_model_, SIGNAL(StreamFinished()), player_, SLOT(Next()));
connect(radio_model_, SIGNAL(StreamReady(QUrl,QUrl)), player_, SLOT(StreamReady(QUrl,QUrl)));

View File

@ -7,6 +7,9 @@ MultiLoadingIndicator::MultiLoadingIndicator(QWidget *parent)
}
void MultiLoadingIndicator::TaskStarted(const QString &name) {
if (tasks_.contains(name))
return;
tasks_ << name;
UpdateText();

View File

@ -272,7 +272,7 @@ QModelIndex Playlist::InsertRadioStations(const QList<RadioItem*>& items, int af
if (!item->playable)
continue;
playlist_items << new RadioPlaylistItem(item->service, item->Url(), item->Title());
playlist_items << new RadioPlaylistItem(item->service, item->Url(), item->Title(), item->Artist());
}
return InsertItems(playlist_items, after);
}

View File

@ -1,7 +1,6 @@
#include "playlistview.h"
#include "playlist.h"
#include "playlistheader.h"
#include "radioloadingindicator.h"
#include "trackslider.h"
#include <QPainter>
@ -125,8 +124,7 @@ PlaylistView::PlaylistView(QWidget *parent)
glow_intensity_step_(0),
row_height_(-1),
currenttrack_play_(":currenttrack_play.png"),
currenttrack_pause_(":currenttrack_pause.png"),
radio_loading_(new RadioLoadingIndicator(this))
currenttrack_pause_(":currenttrack_pause.png")
{
setItemDelegate(new PlaylistDelegateBase(this));
setItemDelegateForColumn(Playlist::Column_Length, new LengthItemDelegate(this));
@ -140,8 +138,6 @@ PlaylistView::PlaylistView(QWidget *parent)
glow_timer_->setInterval(1500 / kGlowIntensitySteps);
connect(glow_timer_, SIGNAL(timeout()), SLOT(GlowIntensityChanged()));
radio_loading_->hide();
}
void PlaylistView::setModel(QAbstractItemModel *model) {
@ -326,24 +322,3 @@ void PlaylistView::contextMenuEvent(QContextMenuEvent* e) {
emit RightClicked(e->globalPos(), indexAt(e->pos()));
e->accept();
}
void PlaylistView::resizeEvent(QResizeEvent *event) {
const QPoint kPadding(5,5);
QPoint pos(viewport()->mapTo(this, viewport()->rect().bottomRight()) -
kPadding - QPoint(radio_loading_->sizeHint().width(),
radio_loading_->sizeHint().height()));
radio_loading_->move(pos);
radio_loading_->resize(radio_loading_->sizeHint());
QTreeView::resizeEvent(event);
}
void PlaylistView::StartRadioLoading() {
radio_loading_->show();
}
void PlaylistView::StopRadioLoading() {
radio_loading_->hide();
}

View File

@ -37,9 +37,6 @@ class PlaylistView : public QTreeView {
public:
PlaylistView(QWidget* parent = 0);
// QWidget
void resizeEvent(QResizeEvent *event);
// QTreeView
void setModel(QAbstractItemModel *model);
void drawRow(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
@ -52,9 +49,6 @@ class PlaylistView : public QTreeView {
void StopGlowing();
void StartGlowing();
void StartRadioLoading();
void StopRadioLoading();
signals:
void PlayPauseItem(const QModelIndex& index);
void RightClicked(const QPoint& global_pos, const QModelIndex& index);
@ -88,8 +82,6 @@ class PlaylistView : public QTreeView {
QList<QPixmap> currenttrack_bar_right_;
QPixmap currenttrack_play_;
QPixmap currenttrack_pause_;
RadioLoadingIndicator* radio_loading_;
};
#endif // PLAYLISTVIEW_H

View File

@ -22,3 +22,7 @@ QUrl RadioItem::Url() const {
QString RadioItem::Title() const {
return service->TitleForItem(this);
}
QString RadioItem::Artist() const {
return service->ArtistForItem(this);
}

View File

@ -21,8 +21,10 @@ class RadioItem : public SimpleTreeItem<RadioItem> {
QUrl Url() const;
QString Title() const;
QString Artist() const;
QIcon icon;
QString artist;
RadioService* service;
bool playable;
};

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RadioLoadingIndicator</class>
<widget class="QWidget" name="RadioLoadingIndicator">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>191</width>
<height>24</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="text"/>
</item>
<item>
<widget class="BusyIndicator" name="spinner">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BusyIndicator</class>
<extends>QLabel</extends>
<header>busyindicator.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -1,6 +1,7 @@
#include "radiomodel.h"
#include "radioservice.h"
#include "lastfmservice.h"
#include "somafmservice.h"
#include "radiomimedata.h"
#include <QMimeData>
@ -16,14 +17,15 @@ RadioModel::RadioModel(QObject* parent)
root_->lazy_loaded = true;
AddService(new LastFMService(this));
AddService(new SomaFMService(this));
}
void RadioModel::AddService(RadioService *service) {
sServices[service->name()] = service;
service->CreateRootItem(root_);
connect(service, SIGNAL(LoadingStarted()), SIGNAL(LoadingStarted()));
connect(service, SIGNAL(LoadingFinished()), SIGNAL(LoadingFinished()));
connect(service, SIGNAL(TaskStarted(QString)), SIGNAL(TaskStarted(QString)));
connect(service, SIGNAL(TaskFinished(QString)), SIGNAL(TaskFinished(QString)));
connect(service, SIGNAL(StreamReady(QUrl,QUrl)), SIGNAL(StreamReady(QUrl,QUrl)));
connect(service, SIGNAL(StreamFinished()), SIGNAL(StreamFinished()));
connect(service, SIGNAL(StreamError(QString)), SIGNAL(StreamError(QString)));

View File

@ -35,8 +35,8 @@ class RadioModel : public SimpleTreeModel<RadioItem> {
void ShowContextMenu(RadioItem* item, const QPoint& global_pos);
signals:
void LoadingStarted();
void LoadingFinished();
void TaskStarted(const QString&);
void TaskFinished(const QString&);
void StreamReady(const QUrl& original_url, const QUrl& media_url);
void StreamFinished();
void StreamError(const QString& message);

View File

@ -10,10 +10,11 @@ RadioPlaylistItem::RadioPlaylistItem()
}
RadioPlaylistItem::RadioPlaylistItem(RadioService* service, const QUrl& url,
const QString& title)
const QString& title, const QString& artist)
: service_(service),
url_(url),
title_(title)
title_(title),
artist_(artist)
{
InitMetadata();
}
@ -22,12 +23,15 @@ void RadioPlaylistItem::Save(QSettings& settings) const {
settings.setValue("service", service_->name());
settings.setValue("url", url_.toString());
settings.setValue("title", title_);
if (!artist_.isEmpty())
settings.setValue("artist", artist_);
}
void RadioPlaylistItem::Restore(const QSettings& settings) {
service_ = RadioModel::ServiceByName(settings.value("service").toString());
url_ = settings.value("url").toString();
title_ = settings.value("title").toString();
artist_ = settings.value("artist").toString();
InitMetadata();
}
@ -39,6 +43,8 @@ void RadioPlaylistItem::InitMetadata() {
metadata_.set_title(title_);
else
metadata_.set_title(url_.toString());
metadata_.set_artist(artist_);
}
Song RadioPlaylistItem::Metadata() const {

View File

@ -11,7 +11,8 @@ class RadioService;
class RadioPlaylistItem : public PlaylistItem {
public:
RadioPlaylistItem();
RadioPlaylistItem(RadioService* service, const QUrl& url, const QString& title);
RadioPlaylistItem(RadioService* service, const QUrl& url,
const QString& title, const QString& artist);
Type type() const { return Type_Radio; }
Options options() const;
@ -36,6 +37,7 @@ class RadioPlaylistItem : public PlaylistItem {
RadioService* service_;
QUrl url_;
QString title_;
QString artist_;
Song metadata_;
Song temp_metadata_;

View File

@ -5,3 +5,19 @@ RadioService::RadioService(const QString& name, QObject *parent)
name_(name)
{
}
QUrl RadioService::UrlForItem(const RadioItem* item) const {
return item->key;
}
QString RadioService::TitleForItem(const RadioItem* item) const {
return item->DisplayText();
}
QString RadioService::ArtistForItem(const RadioItem* item) const {
return item->artist;
}
void RadioService::LoadNext(const QUrl&) {
emit StreamFinished();
}

View File

@ -21,21 +21,23 @@ class RadioService : public QObject {
virtual RadioItem* CreateRootItem(RadioItem* parent) = 0;
virtual void LazyPopulate(RadioItem* item) = 0;
virtual QUrl UrlForItem(const RadioItem* item) const = 0;
virtual QString TitleForItem(const RadioItem* item) const = 0;
virtual QUrl UrlForItem(const RadioItem* item) const;
virtual QString TitleForItem(const RadioItem* item) const;
virtual QString ArtistForItem(const RadioItem* item) const;
virtual void ShowContextMenu(RadioItem* item, const QPoint& global_pos) {
Q_UNUSED(item); Q_UNUSED(global_pos); }
virtual void StartLoading(const QUrl& url) = 0;
virtual void LoadNext(const QUrl& url) = 0;
virtual void LoadNext(const QUrl& url);
virtual bool IsPauseAllowed() const { return true; }
virtual bool ShowLastFmControls() const { return false; }
signals:
void LoadingStarted();
void LoadingFinished();
void TaskStarted(const QString& name);
void TaskFinished(const QString& name);
void StreamReady(const QUrl& original_url, const QUrl& media_url);
void StreamFinished();
void StreamError(const QString& message);

164
src/somafmservice.cpp Normal file
View File

@ -0,0 +1,164 @@
#include "somafmservice.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QXmlStreamReader>
#include <QSettings>
#include <QTemporaryFile>
#include <QCoreApplication>
#include <QtDebug>
const char* SomaFMService::kServiceName = "SomaFM";
const char* SomaFMService::kLoadingChannelsText = "Getting channels";
const char* SomaFMService::kLoadingStreamText = "Loading stream";
const char* SomaFMService::kChannelListUrl = "http://somafm.com/channels.xml";
SomaFMService::SomaFMService(QObject* parent)
: RadioService(kServiceName, parent),
root_(NULL),
network_(new QNetworkAccessManager(this))
{
}
RadioItem* SomaFMService::CreateRootItem(RadioItem* parent) {
root_ = new RadioItem(this, RadioItem::Type_Service, kServiceName, parent);
return root_;
}
void SomaFMService::LazyPopulate(RadioItem* item) {
switch (item->type) {
case RadioItem::Type_Service:
RefreshChannels();
break;
default:
break;
}
item->lazy_loaded = true;
}
void SomaFMService::ShowContextMenu(RadioItem* item, const QPoint& global_pos) {
}
void SomaFMService::StartLoading(const QUrl& url) {
// Load the playlist
QNetworkRequest request = QNetworkRequest(url);
request.setRawHeader("User-Agent", QString("%1 %2").arg(
QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8());
QNetworkReply* reply = network_->get(request);
connect(reply, SIGNAL(finished()), SLOT(LoadPlaylistFinished()));
emit TaskStarted(kLoadingStreamText);
}
void SomaFMService::LoadPlaylistFinished() {
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
emit TaskFinished(kLoadingStreamText);
if (reply->error() != QNetworkReply::NoError) {
// TODO: Error handling
qDebug() << reply->errorString();
return;
}
// TODO: Replace with some more robust .pls parsing :(
QTemporaryFile temp_file;
temp_file.open();
temp_file.write(reply->readAll());
temp_file.flush();
QSettings s(temp_file.fileName(), QSettings::IniFormat);
s.beginGroup("playlist");
emit StreamReady(reply->url().toString(), s.value("File1").toString());
}
void SomaFMService::RefreshChannels() {
QNetworkRequest request = QNetworkRequest(QUrl(kChannelListUrl));
request.setRawHeader("User-Agent", QString("%1 %2").arg(
QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8());
QNetworkReply* reply = network_->get(request);
connect(reply, SIGNAL(finished()), SLOT(RefreshChannelsFinished()));
emit TaskStarted(kLoadingChannelsText);
}
void SomaFMService::RefreshChannelsFinished() {
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
emit TaskFinished(kLoadingChannelsText);
if (reply->error() != QNetworkReply::NoError) {
// TODO: Error handling
qDebug() << reply->errorString();
return;
}
QXmlStreamReader reader(reply);
while (!reader.atEnd()) {
reader.readNext();
if (reader.tokenType() == QXmlStreamReader::StartElement &&
reader.name() == "channel") {
ReadChannel(reader);
}
}
}
void SomaFMService::ReadChannel(QXmlStreamReader& reader) {
RadioItem* item = new RadioItem(this, Type_Stream, QString::null);
item->lazy_loaded = true;
item->playable = true;
while (!reader.atEnd()) {
switch (reader.readNext()) {
case QXmlStreamReader::EndElement:
if (item->key.isNull()) {
// Didn't find a URL
delete item;
} else {
item->InsertNotify(root_);
}
return;
case QXmlStreamReader::StartElement:
if (reader.name() == "title") {
item->display_text = reader.readElementText();
} else if (reader.name() == "dj") {
item->artist = reader.readElementText();
} else if (reader.name() == "fastpls" && reader.attributes().value("format") == "mp3") {
item->key = reader.readElementText();
} else {
ConsumeElement(reader);
}
break;
default:
break;
}
}
delete item;
}
void SomaFMService::ConsumeElement(QXmlStreamReader& reader) {
int level = 1;
while (!reader.atEnd()) {
switch (reader.readNext()) {
case QXmlStreamReader::StartElement: level++; break;
case QXmlStreamReader::EndElement: level--; break;
default: break;
}
if (level == 0)
return;
}
}
QString SomaFMService::TitleForItem(const RadioItem* item) const {
return "SomaFM " + item->display_text;
}

48
src/somafmservice.h Normal file
View File

@ -0,0 +1,48 @@
#ifndef SOMAFMSERVICE_H
#define SOMAFMSERVICE_H
#include "radioservice.h"
class QNetworkAccessManager;
class QXmlStreamReader;
class SomaFMService : public RadioService {
Q_OBJECT
public:
SomaFMService(QObject* parent = 0);
enum ItemType {
Type_Stream = 2000,
};
static const char* kServiceName;
static const char* kLoadingChannelsText;
static const char* kLoadingStreamText;
static const char* kChannelListUrl;
RadioItem* CreateRootItem(RadioItem* parent);
void LazyPopulate(RadioItem* item);
QString TitleForItem(const RadioItem* item) const;
void ShowContextMenu(RadioItem* item, const QPoint& global_pos);
void StartLoading(const QUrl& url);
private slots:
void RefreshChannelsFinished();
void LoadPlaylistFinished();
private:
void RefreshChannels();
void ReadChannel(QXmlStreamReader& reader);
void ConsumeElement(QXmlStreamReader& reader);
private:
RadioItem* root_;
QNetworkAccessManager* network_;
};
#endif // SOMAFMSERVICE_H

View File

@ -1,11 +1,10 @@
# Change this line to install Clementine somewhere else
install_prefix = /usr
VERSION = 0.1
QT += sql \
network \
opengl \
xml
xml \
opengl
TARGET = clementine
TEMPLATE = app
SOURCES += main.cpp \
@ -49,7 +48,8 @@ SOURCES += main.cpp \
trackslider.cpp \
edittagdialog.cpp \
lineedit.cpp \
multiloadingindicator.cpp
multiloadingindicator.cpp \
somafmservice.cpp
HEADERS += mainwindow.h \
player.h \
library.h \
@ -98,12 +98,12 @@ HEADERS += mainwindow.h \
trackslider.h \
edittagdialog.h \
lineedit.h \
multiloadingindicator.h
multiloadingindicator.h \
somafmservice.h
FORMS += mainwindow.ui \
libraryconfig.ui \
fileview.ui \
lastfmconfig.ui \
radioloadingindicator.ui \
lastfmstationdialog.ui \
trackslider.ui \
edittagdialog.ui \
@ -158,6 +158,16 @@ win32:SOURCES += ../3rdparty/qtsingleapplication/qtlockedfile_win32.cpp
# Installs
target.path = $${install_prefix}/bin/
desktop.path = dummy
desktop.extra = xdg-icon-resource install --size 64 ../dist/clementine_64.png application-x-clementine ; \
xdg-desktop-menu install --novendor ../dist/clementine.desktop
INSTALLS += target desktop
desktop.extra = xdg-icon-resource \
install \
--size \
64 \
../dist/clementine_64.png \
application-x-clementine \
; \
xdg-desktop-menu \
install \
--novendor \
../dist/clementine.desktop
INSTALLS += target \
desktop

View File

@ -29,6 +29,9 @@
</item>
<item>
<widget class="QSlider" name="slider">
<property name="singleStep">
<number>10</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>