2010-06-15 15:24:17 +02:00
|
|
|
/* This file is part of Clementine.
|
2014-11-02 19:36:21 +01:00
|
|
|
Copyright 2010-2014, David Sansome <me@davidsansome.com>
|
|
|
|
Copyright 2010-2014, John Maguire <john.maguire@gmail.com>
|
|
|
|
Copyright 2011-2012, 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
|
|
|
|
Copyright 2011, Paweł Bara <keirangtp@gmail.com>
|
|
|
|
Copyright 2014, Alexander Bikadorov <abiku@cs.tu-berlin.de>
|
|
|
|
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
2010-06-15 15:24:17 +02:00
|
|
|
|
|
|
|
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 "songloader.h"
|
2012-11-21 16:03:50 +01:00
|
|
|
|
|
|
|
#include <QBuffer>
|
|
|
|
#include <QDirIterator>
|
|
|
|
#include <QFileInfo>
|
|
|
|
#include <QTimer>
|
|
|
|
#include <QUrl>
|
|
|
|
#include <QtDebug>
|
2020-09-18 16:15:19 +02:00
|
|
|
#include <algorithm>
|
|
|
|
#include <memory>
|
2012-11-21 16:03:50 +01:00
|
|
|
|
|
|
|
#include "config.h"
|
2011-04-22 18:50:29 +02:00
|
|
|
#include "core/logging.h"
|
2014-01-24 13:54:38 +01:00
|
|
|
#include "core/player.h"
|
2012-06-08 15:34:00 +02:00
|
|
|
#include "core/signalchecker.h"
|
2014-01-24 13:54:38 +01:00
|
|
|
#include "core/song.h"
|
2012-01-06 22:27:02 +01:00
|
|
|
#include "core/tagreaderclient.h"
|
2011-11-28 14:51:35 +01:00
|
|
|
#include "core/timeconstants.h"
|
2020-09-18 16:15:19 +02:00
|
|
|
#include "core/utilities.h"
|
2017-03-17 17:55:22 +01:00
|
|
|
#include "core/waitforsignal.h"
|
2014-12-18 23:35:21 +01:00
|
|
|
#include "internet/core/internetmodel.h"
|
2020-09-18 16:15:19 +02:00
|
|
|
#include "internet/lastfm/fixlastfm.h"
|
2017-03-17 17:55:22 +01:00
|
|
|
#include "internet/podcasts/podcastparser.h"
|
|
|
|
#include "internet/podcasts/podcastservice.h"
|
|
|
|
#include "internet/podcasts/podcasturlloader.h"
|
2010-08-31 21:45:33 +02:00
|
|
|
#include "library/librarybackend.h"
|
|
|
|
#include "library/sqlrow.h"
|
2011-01-04 00:36:10 +01:00
|
|
|
#include "playlistparsers/cueparser.h"
|
2014-01-24 13:54:38 +01:00
|
|
|
#include "playlistparsers/parserbase.h"
|
2010-06-15 15:24:17 +02:00
|
|
|
#include "playlistparsers/playlistparser.h"
|
2017-03-17 17:55:22 +01:00
|
|
|
#include "utilities.h"
|
2010-06-15 15:24:17 +02:00
|
|
|
|
2014-11-19 07:45:05 +01:00
|
|
|
#ifdef HAVE_AUDIOCD
|
|
|
|
#include <gst/audio/gstaudiocdsrc.h>
|
2020-09-18 16:15:19 +02:00
|
|
|
|
2014-11-19 07:45:05 +01:00
|
|
|
#include "devices/cddasongloader.h"
|
|
|
|
#endif
|
|
|
|
|
2014-02-06 14:48:00 +01:00
|
|
|
using std::placeholders::_1;
|
2011-06-10 01:08:43 +02:00
|
|
|
|
2010-06-23 13:47:54 +02:00
|
|
|
QSet<QString> SongLoader::sRawUriSchemes;
|
2010-06-23 13:51:13 +02:00
|
|
|
const int SongLoader::kDefaultTimeout = 5000;
|
2010-06-23 13:47:54 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
SongLoader::SongLoader(LibraryBackendInterface* library, const Player* player,
|
|
|
|
QObject* parent)
|
|
|
|
: QObject(parent),
|
|
|
|
timeout_timer_(new QTimer(this)),
|
|
|
|
playlist_parser_(new PlaylistParser(library, this)),
|
|
|
|
podcast_parser_(new PodcastParser),
|
|
|
|
cue_parser_(new CueParser(library, this)),
|
|
|
|
timeout_(kDefaultTimeout),
|
|
|
|
state_(WaitingForType),
|
|
|
|
success_(false),
|
|
|
|
parser_(nullptr),
|
|
|
|
is_podcast_(false),
|
|
|
|
library_(library),
|
|
|
|
player_(player) {
|
2010-06-23 13:47:54 +02:00
|
|
|
if (sRawUriSchemes.isEmpty()) {
|
2014-02-07 16:34:20 +01:00
|
|
|
sRawUriSchemes << "udp"
|
|
|
|
<< "mms"
|
|
|
|
<< "mmsh"
|
|
|
|
<< "mmst"
|
|
|
|
<< "mmsu"
|
|
|
|
<< "rtsp"
|
|
|
|
<< "rtspu"
|
|
|
|
<< "rtspt"
|
|
|
|
<< "rtsph"
|
|
|
|
<< "spotify";
|
2010-06-23 13:47:54 +02:00
|
|
|
}
|
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
timeout_timer_->setSingleShot(true);
|
2010-06-26 15:20:08 +02:00
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
connect(timeout_timer_, SIGNAL(timeout()), SLOT(Timeout()));
|
|
|
|
}
|
|
|
|
|
2010-06-15 20:24:08 +02:00
|
|
|
SongLoader::~SongLoader() {
|
|
|
|
if (pipeline_) {
|
|
|
|
state_ = Finished;
|
|
|
|
gst_element_set_state(pipeline_.get(), GST_STATE_NULL);
|
|
|
|
}
|
2012-03-11 18:57:15 +01:00
|
|
|
|
|
|
|
delete podcast_parser_;
|
2010-06-15 20:24:08 +02:00
|
|
|
}
|
|
|
|
|
2010-06-23 13:51:13 +02:00
|
|
|
SongLoader::Result SongLoader::Load(const QUrl& url) {
|
2010-06-15 15:24:17 +02:00
|
|
|
url_ = url;
|
|
|
|
|
|
|
|
if (url_.scheme() == "file") {
|
2010-08-31 21:45:33 +02:00
|
|
|
return LoadLocal(url_.toLocalFile());
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
2014-01-24 13:54:38 +01:00
|
|
|
if (sRawUriSchemes.contains(url_.scheme()) ||
|
|
|
|
player_->HandlerForUrl(url) != nullptr) {
|
|
|
|
// The URI scheme indicates that it can't possibly be a playlist, or we have
|
|
|
|
// a custom handler for the URL, so add it as a raw stream.
|
2010-06-23 13:47:54 +02:00
|
|
|
AddAsRawStream();
|
|
|
|
return Success;
|
|
|
|
}
|
|
|
|
|
2017-03-17 03:23:32 +01:00
|
|
|
// It could be a playlist, we give it a shot.
|
|
|
|
if (LoadRemotePlaylist(url_)) {
|
|
|
|
return Success;
|
|
|
|
}
|
|
|
|
|
2012-03-11 19:14:53 +01:00
|
|
|
url_ = PodcastUrlLoader::FixPodcastUrl(url_);
|
|
|
|
|
2014-04-02 15:57:01 +02:00
|
|
|
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
|
2014-04-07 15:27:47 +02:00
|
|
|
return BlockingLoadRequired;
|
2014-04-02 15:57:01 +02:00
|
|
|
}
|
|
|
|
|
2019-04-07 23:18:11 +02:00
|
|
|
SongLoader::Result SongLoader::LoadFilenamesBlocking() {
|
2014-04-02 15:57:01 +02:00
|
|
|
if (preload_func_) {
|
2019-04-07 23:18:11 +02:00
|
|
|
return preload_func_();
|
|
|
|
} else {
|
|
|
|
qLog(Error) << "Preload function was not set for blocking operation";
|
|
|
|
return Error;
|
2014-04-02 15:57:01 +02:00
|
|
|
}
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
2011-04-16 16:04:15 +02:00
|
|
|
SongLoader::Result SongLoader::LoadLocalPartial(const QString& filename) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << "Fast Loading local file" << filename;
|
2011-04-16 16:04:15 +02:00
|
|
|
// First check to see if it's a directory - if so we can load all the songs
|
|
|
|
// inside right away.
|
|
|
|
if (QFileInfo(filename).isDir()) {
|
|
|
|
LoadLocalDirectory(filename);
|
|
|
|
return Success;
|
|
|
|
}
|
|
|
|
Song song;
|
|
|
|
song.InitFromFilePartial(filename);
|
2014-02-07 16:34:20 +01:00
|
|
|
if (song.is_valid()) songs_ << song;
|
2011-04-16 16:04:15 +02:00
|
|
|
return Success;
|
|
|
|
}
|
|
|
|
|
2011-06-10 01:08:43 +02:00
|
|
|
SongLoader::Result SongLoader::LoadAudioCD() {
|
2014-10-26 03:32:37 +01:00
|
|
|
#ifdef HAVE_AUDIOCD
|
|
|
|
CddaSongLoader* cdda_song_loader = new CddaSongLoader;
|
2017-03-17 03:23:32 +01:00
|
|
|
connect(cdda_song_loader, SIGNAL(SongsDurationLoaded(SongList)), this,
|
|
|
|
SLOT(AudioCDTracksLoadedSlot(SongList)));
|
|
|
|
connect(cdda_song_loader, SIGNAL(SongsMetadataLoaded(SongList)), this,
|
|
|
|
SLOT(AudioCDTracksTagsLoaded(SongList)));
|
2014-10-26 03:32:37 +01:00
|
|
|
cdda_song_loader->LoadSongs();
|
|
|
|
return Success;
|
|
|
|
#else // HAVE_AUDIOCD
|
2011-08-10 17:23:32 +02:00
|
|
|
return Error;
|
2014-10-26 03:32:37 +01:00
|
|
|
#endif
|
2011-06-10 01:08:43 +02:00
|
|
|
}
|
|
|
|
|
2014-10-26 03:32:37 +01:00
|
|
|
#ifdef HAVE_AUDIOCD
|
|
|
|
void SongLoader::AudioCDTracksLoadedSlot(const SongList& songs) {
|
|
|
|
songs_ = songs;
|
|
|
|
emit AudioCDTracksLoaded();
|
|
|
|
}
|
|
|
|
|
|
|
|
void SongLoader::AudioCDTracksTagsLoaded(const SongList& songs) {
|
|
|
|
CddaSongLoader* cdda_song_loader = qobject_cast<CddaSongLoader*>(sender());
|
|
|
|
cdda_song_loader->deleteLater();
|
|
|
|
songs_ = songs;
|
2014-04-07 15:27:47 +02:00
|
|
|
emit LoadAudioCDFinished(true);
|
2011-06-10 01:08:43 +02:00
|
|
|
}
|
2014-11-01 19:26:05 +01:00
|
|
|
#endif // HAVE_AUDIOCD
|
2011-06-10 01:08:43 +02:00
|
|
|
|
2013-02-17 08:10:08 +01:00
|
|
|
SongLoader::Result SongLoader::LoadLocal(const QString& filename) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << "Loading local file" << filename;
|
2010-06-15 20:24:08 +02:00
|
|
|
|
2014-04-02 15:57:01 +02:00
|
|
|
// Search in the database.
|
|
|
|
QUrl url = QUrl::fromLocalFile(filename);
|
|
|
|
|
|
|
|
LibraryQuery query;
|
|
|
|
query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
|
|
|
|
query.AddWhere("filename", url.toEncoded());
|
|
|
|
|
|
|
|
if (library_->ExecQuery(&query) && query.Next()) {
|
|
|
|
// we may have many results when the file has many sections
|
|
|
|
do {
|
|
|
|
Song song;
|
|
|
|
song.InitFromQuery(query, true);
|
|
|
|
|
|
|
|
if (song.is_valid()) {
|
|
|
|
songs_ << song;
|
|
|
|
}
|
|
|
|
} while (query.Next());
|
|
|
|
|
|
|
|
return Success;
|
|
|
|
}
|
|
|
|
|
|
|
|
// It's not in the database, load it asynchronously.
|
2017-03-17 03:23:32 +01:00
|
|
|
preload_func_ = std::bind(&SongLoader::LoadLocalAsync, this, filename);
|
2014-04-07 15:27:47 +02:00
|
|
|
return BlockingLoadRequired;
|
2014-04-02 15:57:01 +02:00
|
|
|
}
|
|
|
|
|
2019-04-07 23:18:11 +02:00
|
|
|
SongLoader::Result SongLoader::LoadLocalAsync(const QString& filename) {
|
2014-04-02 15:57:01 +02:00
|
|
|
// First check to see if it's a directory - if so we will load all the songs
|
2010-06-15 15:24:17 +02:00
|
|
|
// inside right away.
|
2021-04-28 08:40:30 +02:00
|
|
|
QFileInfo info(filename);
|
2021-04-26 17:40:09 +02:00
|
|
|
if (info.isDir()) {
|
2014-04-02 15:57:01 +02:00
|
|
|
LoadLocalDirectory(filename);
|
2019-04-07 23:18:11 +02:00
|
|
|
return Success;
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// It's a local file, so check if it looks like a playlist.
|
|
|
|
// Read the first few bytes.
|
|
|
|
QFile file(filename);
|
2019-04-07 23:18:11 +02:00
|
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
|
|
qLog(Error) << "Could not open file " << filename;
|
|
|
|
return Error;
|
|
|
|
}
|
2010-06-15 15:24:17 +02:00
|
|
|
QByteArray data(file.read(PlaylistParser::kMagicSize));
|
|
|
|
|
2011-02-27 13:14:32 +01:00
|
|
|
ParserBase* parser = playlist_parser_->ParserForMagic(data);
|
2010-07-24 16:09:27 +02:00
|
|
|
if (!parser) {
|
|
|
|
// Check the file extension as well, maybe the magic failed, or it was a
|
|
|
|
// basic M3U file which is just a plain list of filenames.
|
2017-03-17 03:23:32 +01:00
|
|
|
parser = playlist_parser_->ParserForExtension(
|
|
|
|
QFileInfo(filename).suffix().toLower());
|
2010-07-24 16:09:27 +02:00
|
|
|
}
|
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
if (parser) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << "Parsing using" << parser->name();
|
2010-06-15 20:24:08 +02:00
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
// It's a playlist!
|
2021-04-25 21:54:47 +02:00
|
|
|
file.reset();
|
|
|
|
songs_ = parser->Load(&file, filename, info.path());
|
2019-04-07 23:18:11 +02:00
|
|
|
return Success;
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
2014-04-02 15:57:01 +02:00
|
|
|
// Check if it's a cue file
|
|
|
|
QString matching_cue = filename.section('.', 0, -2) + ".cue";
|
|
|
|
if (QFile::exists(matching_cue)) {
|
|
|
|
// it's a cue - create virtual tracks
|
|
|
|
QFile cue(matching_cue);
|
|
|
|
cue.open(QIODevice::ReadOnly);
|
2011-01-04 00:36:10 +01:00
|
|
|
|
2014-04-02 15:57:01 +02:00
|
|
|
SongList song_list = cue_parser_->Load(&cue, matching_cue,
|
|
|
|
QDir(filename.section('/', 0, -2)));
|
2021-04-28 21:15:15 +02:00
|
|
|
for (const Song& song : song_list) {
|
2014-04-02 15:57:01 +02:00
|
|
|
if (song.is_valid()) songs_ << song;
|
2013-02-17 08:19:05 +01:00
|
|
|
}
|
2019-04-07 23:18:11 +02:00
|
|
|
return Success;
|
2011-01-04 00:36:10 +01:00
|
|
|
}
|
|
|
|
|
2014-04-02 15:57:01 +02:00
|
|
|
// Assume it's just a normal file
|
2013-02-17 08:19:05 +01:00
|
|
|
Song song;
|
2014-04-02 15:57:01 +02:00
|
|
|
song.InitFromFilePartial(filename);
|
2014-11-01 19:26:05 +01:00
|
|
|
if (song.is_valid()) {
|
|
|
|
songs_ << song;
|
2019-04-07 23:18:11 +02:00
|
|
|
return Success;
|
|
|
|
} else {
|
|
|
|
return Error;
|
2014-11-01 19:26:05 +01:00
|
|
|
}
|
2013-02-17 08:19:05 +01:00
|
|
|
}
|
|
|
|
|
2014-04-07 15:27:47 +02:00
|
|
|
void SongLoader::LoadMetadataBlocking() {
|
2011-04-16 16:04:15 +02:00
|
|
|
for (int i = 0; i < songs_.size(); i++) {
|
2012-07-14 00:53:42 +02:00
|
|
|
EffectiveSongLoad(&songs_[i]);
|
|
|
|
}
|
|
|
|
}
|
2011-04-16 16:04:15 +02:00
|
|
|
|
2012-07-14 00:53:42 +02:00
|
|
|
void SongLoader::EffectiveSongLoad(Song* song) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!song) return;
|
2011-08-27 23:29:35 +02:00
|
|
|
|
2012-07-14 00:53:42 +02:00
|
|
|
if (song->filetype() != Song::Type_Unknown) {
|
|
|
|
// Maybe we loaded the metadata already, for example from a cuesheet.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// First, try to get the song from the library
|
|
|
|
Song library_song = library_->GetSongByUrl(song->url());
|
|
|
|
if (library_song.is_valid()) {
|
|
|
|
*song = library_song;
|
|
|
|
} else {
|
|
|
|
// it's a normal media file
|
|
|
|
QString filename = song->url().toLocalFile();
|
|
|
|
TagReaderClient::Instance()->ReadFileBlocking(filename, song);
|
2011-04-16 16:04:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-06-15 20:24:08 +02:00
|
|
|
static bool CompareSongs(const Song& left, const Song& right) {
|
2010-06-15 20:34:15 +02:00
|
|
|
// Order by artist, album, disc, track
|
2010-06-15 20:24:08 +02:00
|
|
|
if (left.artist() < right.artist()) return true;
|
2010-12-18 19:47:44 +01:00
|
|
|
if (left.artist() > right.artist()) return false;
|
2010-06-15 20:24:08 +02:00
|
|
|
if (left.album() < right.album()) return true;
|
2010-12-18 19:47:44 +01:00
|
|
|
if (left.album() > right.album()) return false;
|
2010-06-15 20:24:08 +02:00
|
|
|
if (left.disc() < right.disc()) return true;
|
2010-12-18 19:47:44 +01:00
|
|
|
if (left.disc() > right.disc()) return false;
|
|
|
|
if (left.track() < right.track()) return true;
|
|
|
|
if (left.track() > right.track()) return false;
|
2011-04-28 14:27:53 +02:00
|
|
|
return left.url() < right.url();
|
2010-06-15 20:24:08 +02:00
|
|
|
}
|
|
|
|
|
2010-06-15 16:24:17 +02:00
|
|
|
void SongLoader::LoadLocalDirectory(const QString& filename) {
|
2010-06-15 15:24:17 +02:00
|
|
|
QDirIterator it(filename, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable,
|
|
|
|
QDirIterator::Subdirectories);
|
|
|
|
|
|
|
|
while (it.hasNext()) {
|
2011-04-16 16:04:15 +02:00
|
|
|
LoadLocalPartial(it.next());
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
2018-10-05 17:19:05 +02:00
|
|
|
std::stable_sort(songs_.begin(), songs_.end(), CompareSongs);
|
2012-07-14 00:53:42 +02:00
|
|
|
|
|
|
|
// Load the first song: all songs will be loaded async, but we want the first
|
|
|
|
// one in our list to be fully loaded, so if the user has the "Start playing
|
|
|
|
// when adding to playlist" preference behaviour set, it can enjoy the first
|
|
|
|
// song being played (seek it, have moodbar, etc.)
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!songs_.isEmpty()) EffectiveSongLoad(&(*songs_.begin()));
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
2010-06-26 15:20:08 +02:00
|
|
|
void SongLoader::AddAsRawStream() {
|
|
|
|
Song song;
|
|
|
|
song.set_valid(true);
|
|
|
|
song.set_filetype(Song::Type_Stream);
|
2011-04-28 14:27:53 +02:00
|
|
|
song.set_url(url_);
|
2010-06-26 15:20:08 +02:00
|
|
|
song.set_title(url_.toString());
|
|
|
|
songs_ << song;
|
|
|
|
}
|
|
|
|
|
2010-06-26 17:09:32 +02:00
|
|
|
void SongLoader::Timeout() {
|
|
|
|
state_ = Finished;
|
|
|
|
success_ = false;
|
|
|
|
StopTypefind();
|
|
|
|
}
|
|
|
|
|
|
|
|
void SongLoader::StopTypefind() {
|
|
|
|
// Destroy the pipeline
|
|
|
|
if (pipeline_) {
|
|
|
|
gst_element_set_state(pipeline_.get(), GST_STATE_NULL);
|
|
|
|
pipeline_.reset();
|
|
|
|
}
|
|
|
|
timeout_timer_->stop();
|
|
|
|
|
|
|
|
if (success_ && parser_) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << "Parsing" << url_ << "with" << parser_->name();
|
2010-06-26 17:09:32 +02:00
|
|
|
|
|
|
|
// Parse the playlist
|
|
|
|
QBuffer buf(&buffer_);
|
|
|
|
buf.open(QIODevice::ReadOnly);
|
|
|
|
songs_ = parser_->Load(&buf);
|
2012-03-11 18:57:15 +01:00
|
|
|
} else if (success_ && is_podcast_) {
|
|
|
|
qLog(Debug) << "Parsing" << url_ << "as a podcast";
|
|
|
|
|
|
|
|
QBuffer buf(&buffer_);
|
|
|
|
buf.open(QIODevice::ReadOnly);
|
|
|
|
QVariant result = podcast_parser_->Load(&buf, url_);
|
|
|
|
|
|
|
|
if (result.isNull()) {
|
|
|
|
qLog(Warning) << "Failed to parse podcast";
|
|
|
|
} else {
|
|
|
|
InternetModel::Service<PodcastService>()->SubscribeAndShow(result);
|
|
|
|
}
|
|
|
|
|
2010-06-26 17:09:32 +02:00
|
|
|
} else if (success_) {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << "Loading" << url_ << "as raw stream";
|
2010-06-26 17:09:32 +02:00
|
|
|
|
|
|
|
// It wasn't a playlist - just put the URL in as a stream
|
|
|
|
AddAsRawStream();
|
|
|
|
}
|
|
|
|
|
2014-04-07 15:27:47 +02:00
|
|
|
emit LoadRemoteFinished();
|
2010-06-26 17:09:32 +02:00
|
|
|
}
|
|
|
|
|
2019-04-07 23:18:11 +02:00
|
|
|
SongLoader::Result SongLoader::LoadRemote() {
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << "Loading remote file" << url_;
|
2010-06-15 20:24:08 +02:00
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
// It's not a local file so we have to fetch it to see what it is. We use
|
|
|
|
// gstreamer to do this since it handles funky URLs for us (http://, ssh://,
|
|
|
|
// etc) and also has typefinder plugins.
|
|
|
|
// First we wait for typefinder to tell us what it is. If it's not text/plain
|
|
|
|
// or text/uri-list assume it's a song and return success.
|
|
|
|
// Otherwise wait to get 512 bytes of data and do magic on it - if the magic
|
2010-06-15 15:28:08 +02:00
|
|
|
// fails then we don't know what it is so return failure.
|
2010-06-15 15:24:17 +02:00
|
|
|
// If the magic succeeds then we know for sure it's a playlist - so read the
|
|
|
|
// rest of the file, parse the playlist and return success.
|
|
|
|
|
2014-04-02 15:57:01 +02:00
|
|
|
timeout_timer_->start(timeout_);
|
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
// Create the pipeline - it gets unreffed if it goes out of scope
|
2014-02-07 16:34:20 +01:00
|
|
|
std::shared_ptr<GstElement> pipeline(gst_pipeline_new(nullptr),
|
|
|
|
std::bind(&gst_object_unref, _1));
|
2010-06-15 15:24:17 +02:00
|
|
|
|
|
|
|
// Create the source element automatically based on the URL
|
|
|
|
GstElement* source = gst_element_make_from_uri(
|
2020-09-18 16:15:19 +02:00
|
|
|
GST_URI_SRC, Utilities::GetUriForGstreamer(url_).constData(), nullptr,
|
|
|
|
nullptr);
|
2010-06-15 15:24:17 +02:00
|
|
|
if (!source) {
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Warning) << "Couldn't create gstreamer source element for"
|
|
|
|
<< url_.toString();
|
2019-04-07 23:18:11 +02:00
|
|
|
return Error;
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create the other elements and link them up
|
2014-02-06 16:49:49 +01:00
|
|
|
GstElement* typefind = gst_element_factory_make("typefind", nullptr);
|
|
|
|
GstElement* fakesink = gst_element_factory_make("fakesink", nullptr);
|
2010-06-15 15:24:17 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
gst_bin_add_many(GST_BIN(pipeline.get()), source, typefind, fakesink,
|
|
|
|
nullptr);
|
2014-02-06 16:49:49 +01:00
|
|
|
gst_element_link_many(source, typefind, fakesink, nullptr);
|
2010-06-15 15:24:17 +02:00
|
|
|
|
|
|
|
// Connect callbacks
|
|
|
|
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline.get()));
|
2012-06-08 15:34:00 +02:00
|
|
|
CHECKED_GCONNECT(typefind, "have-type", &TypeFound, this);
|
2013-09-25 15:42:13 +02:00
|
|
|
gst_bus_set_sync_handler(bus, BusCallbackSync, this, NULL);
|
2010-06-15 15:24:17 +02:00
|
|
|
gst_bus_add_watch(bus, BusCallback, this);
|
|
|
|
|
|
|
|
// Add a probe to the sink so we can capture the data if it's a playlist
|
2013-01-29 13:19:26 +01:00
|
|
|
GstPad* pad = gst_element_get_static_pad(fakesink, "sink");
|
2017-03-17 03:23:32 +01:00
|
|
|
gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, &DataReady, this, NULL);
|
2010-06-15 15:24:17 +02:00
|
|
|
gst_object_unref(pad);
|
|
|
|
|
2014-04-02 15:57:01 +02:00
|
|
|
QEventLoop loop;
|
2014-04-07 15:27:47 +02:00
|
|
|
loop.connect(this, SIGNAL(LoadRemoteFinished()), SLOT(quit()));
|
2014-04-02 15:57:01 +02:00
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
// Start "playing"
|
|
|
|
gst_element_set_state(pipeline.get(), GST_STATE_PLAYING);
|
|
|
|
pipeline_ = pipeline;
|
2014-04-02 15:57:01 +02:00
|
|
|
|
|
|
|
// Wait until loading is finished
|
|
|
|
loop.exec();
|
2019-04-07 23:18:11 +02:00
|
|
|
return Success;
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void SongLoader::TypeFound(GstElement*, uint, GstCaps* caps, void* self) {
|
|
|
|
SongLoader* instance = static_cast<SongLoader*>(self);
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
if (instance->state_ != WaitingForType) return;
|
2010-06-15 15:24:17 +02:00
|
|
|
|
|
|
|
// Check the mimetype
|
2014-02-07 16:34:20 +01:00
|
|
|
instance->mime_type_ =
|
|
|
|
gst_structure_get_name(gst_caps_get_structure(caps, 0));
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << "Mime type is" << instance->mime_type_;
|
2010-06-15 16:52:42 +02:00
|
|
|
if (instance->mime_type_ == "text/plain" ||
|
2010-11-26 16:16:48 +01:00
|
|
|
instance->mime_type_ == "text/uri-list" ||
|
2014-02-07 16:34:20 +01:00
|
|
|
instance->podcast_parser_->supported_mime_types().contains(
|
|
|
|
instance->mime_type_)) {
|
2010-06-15 15:24:17 +02:00
|
|
|
// Yeah it might be a playlist, let's get some data and have a better look
|
|
|
|
instance->state_ = WaitingForMagic;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Nope, not a playlist - we're done
|
|
|
|
instance->StopTypefindAsync(true);
|
|
|
|
}
|
|
|
|
|
2017-03-17 03:23:32 +01:00
|
|
|
GstPadProbeReturn SongLoader::DataReady(GstPad*, GstPadProbeInfo* info,
|
|
|
|
gpointer self) {
|
2013-09-25 15:42:13 +02:00
|
|
|
SongLoader* instance = reinterpret_cast<SongLoader*>(self);
|
2010-06-15 15:24:17 +02:00
|
|
|
|
2017-03-17 03:23:32 +01:00
|
|
|
if (instance->state_ == Finished) return GST_PAD_PROBE_OK;
|
2013-09-25 15:42:13 +02:00
|
|
|
|
|
|
|
GstBuffer* buffer = gst_pad_probe_info_get_buffer(info);
|
|
|
|
GstMapInfo map;
|
|
|
|
gst_buffer_map(buffer, &map, GST_MAP_READ);
|
2010-06-15 15:24:17 +02:00
|
|
|
|
|
|
|
// Append the data to the buffer
|
2013-09-25 15:42:13 +02:00
|
|
|
instance->buffer_.append(reinterpret_cast<const char*>(map.data), map.size);
|
2011-04-22 18:50:29 +02:00
|
|
|
qLog(Debug) << "Received total" << instance->buffer_.size() << "bytes";
|
2013-09-25 15:42:13 +02:00
|
|
|
gst_buffer_unmap(buffer, &map);
|
2010-06-15 15:24:17 +02:00
|
|
|
|
|
|
|
if (instance->state_ == WaitingForMagic &&
|
2011-11-10 21:42:06 +01:00
|
|
|
(instance->buffer_.size() >= PlaylistParser::kMagicSize ||
|
|
|
|
!instance->IsPipelinePlaying())) {
|
2010-06-15 15:24:17 +02:00
|
|
|
// Got enough that we can test the magic
|
|
|
|
instance->MagicReady();
|
|
|
|
}
|
2012-03-11 18:20:12 +01:00
|
|
|
|
2013-09-25 15:42:13 +02:00
|
|
|
return GST_PAD_PROBE_OK;
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
gboolean SongLoader::BusCallback(GstBus*, GstMessage* msg, gpointer self) {
|
|
|
|
SongLoader* instance = reinterpret_cast<SongLoader*>(self);
|
|
|
|
|
|
|
|
switch (GST_MESSAGE_TYPE(msg)) {
|
|
|
|
case GST_MESSAGE_ERROR:
|
|
|
|
instance->ErrorMessageReceived(msg);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2012-03-11 18:20:12 +01:00
|
|
|
return TRUE;
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
GstBusSyncReply SongLoader::BusCallbackSync(GstBus*, GstMessage* msg,
|
|
|
|
gpointer self) {
|
2010-06-15 15:24:17 +02:00
|
|
|
SongLoader* instance = reinterpret_cast<SongLoader*>(self);
|
2012-03-11 18:20:12 +01:00
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
switch (GST_MESSAGE_TYPE(msg)) {
|
|
|
|
case GST_MESSAGE_EOS:
|
|
|
|
instance->EndOfStreamReached();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case GST_MESSAGE_ERROR:
|
|
|
|
instance->ErrorMessageReceived(msg);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return GST_BUS_PASS;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SongLoader::ErrorMessageReceived(GstMessage* msg) {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (state_ == Finished) return;
|
2010-06-15 15:24:17 +02:00
|
|
|
|
|
|
|
GError* error;
|
|
|
|
gchar* debugs;
|
|
|
|
|
|
|
|
gst_message_parse_error(msg, &error, &debugs);
|
2018-12-02 10:51:19 +01:00
|
|
|
qLog(Error) << QString::fromLocal8Bit(error->message);
|
|
|
|
qLog(Error) << QString::fromLocal8Bit(debugs);
|
2010-06-15 15:24:17 +02:00
|
|
|
|
2010-07-10 20:39:41 +02:00
|
|
|
QString message_str = error->message;
|
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
g_error_free(error);
|
|
|
|
free(debugs);
|
|
|
|
|
2010-07-10 20:39:41 +02:00
|
|
|
if (state_ == WaitingForType &&
|
2014-02-07 16:34:20 +01:00
|
|
|
message_str == gst_error_get_message(GST_STREAM_ERROR,
|
|
|
|
GST_STREAM_ERROR_TYPE_NOT_FOUND)) {
|
2010-07-10 20:39:41 +02:00
|
|
|
// Don't give up - assume it's a playlist and see if one of our parsers can
|
|
|
|
// read it.
|
|
|
|
state_ = WaitingForMagic;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
StopTypefindAsync(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SongLoader::EndOfStreamReached() {
|
2011-11-10 21:42:06 +01:00
|
|
|
qLog(Debug) << Q_FUNC_INFO << state_;
|
2010-06-15 15:24:17 +02:00
|
|
|
switch (state_) {
|
2014-02-07 16:34:20 +01:00
|
|
|
case Finished:
|
2010-06-15 15:24:17 +02:00
|
|
|
break;
|
2014-02-07 16:34:20 +01:00
|
|
|
|
|
|
|
case WaitingForMagic:
|
|
|
|
// Do the magic on the data we have already
|
|
|
|
MagicReady();
|
|
|
|
if (state_ == Finished) break;
|
2010-06-15 15:24:17 +02:00
|
|
|
// It looks like a playlist, so parse it
|
|
|
|
|
|
|
|
// fallthrough
|
2014-02-07 16:34:20 +01:00
|
|
|
case WaitingForData:
|
|
|
|
// It's a playlist and we've got all the data - finish and parse it
|
|
|
|
StopTypefindAsync(true);
|
|
|
|
break;
|
2010-06-15 15:24:17 +02:00
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
case WaitingForType:
|
|
|
|
StopTypefindAsync(false);
|
|
|
|
break;
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SongLoader::MagicReady() {
|
2011-11-10 21:42:06 +01:00
|
|
|
qLog(Debug) << Q_FUNC_INFO;
|
2011-02-27 13:14:32 +01:00
|
|
|
parser_ = playlist_parser_->ParserForMagic(buffer_, mime_type_);
|
2012-03-11 18:57:15 +01:00
|
|
|
is_podcast_ = false;
|
2010-06-15 15:24:17 +02:00
|
|
|
|
|
|
|
if (!parser_) {
|
2012-03-11 18:57:15 +01:00
|
|
|
// Maybe it's a podcast?
|
|
|
|
if (podcast_parser_->TryMagic(buffer_)) {
|
|
|
|
is_podcast_ = true;
|
|
|
|
qLog(Debug) << "Looks like a podcast";
|
|
|
|
} else {
|
2014-02-07 16:34:20 +01:00
|
|
|
qLog(Warning) << url_.toString()
|
|
|
|
<< "is text, but not a recognised playlist";
|
2012-03-11 18:57:15 +01:00
|
|
|
// It doesn't look like a playlist, so just finish
|
|
|
|
StopTypefindAsync(false);
|
|
|
|
return;
|
|
|
|
}
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
2012-03-11 18:57:15 +01:00
|
|
|
// We'll get more data and parse the whole thing in EndOfStreamReached
|
|
|
|
|
|
|
|
if (!is_podcast_) {
|
|
|
|
qLog(Debug) << "Magic says" << parser_->name();
|
|
|
|
if (parser_->name() == "ASX/INI" && url_.scheme() == "http") {
|
2014-02-07 16:34:20 +01:00
|
|
|
// This is actually a weird MS-WMSP stream. Changing the protocol to MMS
|
|
|
|
// from
|
2012-03-11 18:57:15 +01:00
|
|
|
// HTTP makes it playable.
|
2014-02-06 16:49:49 +01:00
|
|
|
parser_ = nullptr;
|
2012-03-11 18:57:15 +01:00
|
|
|
url_.setScheme("mms");
|
|
|
|
StopTypefindAsync(true);
|
|
|
|
}
|
2010-10-25 14:14:28 +02:00
|
|
|
}
|
2012-03-11 18:57:15 +01:00
|
|
|
|
2010-06-15 15:24:17 +02:00
|
|
|
state_ = WaitingForData;
|
2011-11-10 21:42:06 +01:00
|
|
|
|
|
|
|
if (!IsPipelinePlaying()) {
|
|
|
|
EndOfStreamReached();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SongLoader::IsPipelinePlaying() {
|
2012-03-11 18:20:12 +01:00
|
|
|
GstState state = GST_STATE_NULL;
|
|
|
|
GstState pending_state = GST_STATE_NULL;
|
2014-02-07 16:34:20 +01:00
|
|
|
GstStateChangeReturn ret = gst_element_get_state(pipeline_.get(), &state,
|
|
|
|
&pending_state, GST_SECOND);
|
2012-03-11 18:20:12 +01:00
|
|
|
|
|
|
|
if (ret == GST_STATE_CHANGE_ASYNC && pending_state == GST_STATE_PLAYING) {
|
|
|
|
// We're still on the way to playing
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return state == GST_STATE_PLAYING;
|
2010-06-15 15:24:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void SongLoader::StopTypefindAsync(bool success) {
|
|
|
|
state_ = Finished;
|
|
|
|
success_ = success;
|
|
|
|
|
|
|
|
metaObject()->invokeMethod(this, "StopTypefind", Qt::QueuedConnection);
|
|
|
|
}
|
2017-03-17 03:23:32 +01:00
|
|
|
|
|
|
|
bool SongLoader::LoadRemotePlaylist(const QUrl& url) {
|
|
|
|
// This function makes a remote request for the given URL and, if its MIME
|
2021-04-28 08:40:30 +02:00
|
|
|
// type corresponds to a known playlist type, it loads it, and returns true.
|
2017-03-17 03:23:32 +01:00
|
|
|
// If the URL does not point to a playlist file we could handle,
|
|
|
|
// it returns false.
|
|
|
|
|
|
|
|
NetworkAccessManager manager;
|
2021-04-25 21:54:47 +02:00
|
|
|
QNetworkRequest req(url);
|
2017-03-17 03:23:32 +01:00
|
|
|
|
|
|
|
// Getting headers:
|
2017-03-17 17:55:22 +01:00
|
|
|
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();
|
2017-03-17 03:23:32 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now we check if there is a parser that can handle that MIME type.
|
|
|
|
QString mime_type =
|
2017-03-17 17:55:22 +01:00
|
|
|
headers_reply->header(QNetworkRequest::ContentTypeHeader).toString();
|
2017-03-17 03:23:32 +01:00
|
|
|
|
2017-03-17 17:55:22 +01:00
|
|
|
ParserBase* const parser = playlist_parser_->ParserForMimeType(mime_type);
|
2017-03-17 03:23:32 +01:00
|
|
|
if (parser == nullptr) {
|
|
|
|
qLog(Debug) << url.toString() << "seems to not be a playlist";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We know it is a playlist!
|
|
|
|
// Getting its contents:
|
2017-03-17 17:55:22 +01:00
|
|
|
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();
|
2017-03-17 03:23:32 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-04-26 16:04:42 +02:00
|
|
|
qLog(Debug) << "Loading" << url.toString() << "with MIME" << mime_type;
|
|
|
|
|
2021-04-27 10:16:36 +02:00
|
|
|
songs_ = parser->Load(data_reply);
|
2017-03-17 03:23:32 +01:00
|
|
|
return true;
|
|
|
|
}
|