Commit 9fc4ad61 authored by Camilo Higuita's avatar Camilo Higuita

initial work on youtube video integration

parent 4eb85928
......@@ -7,6 +7,7 @@ QT += network
QT += xml
QT += qml
QT += quickcontrols2
QT += webkit
TARGET = babe
TEMPLATE = app
......@@ -61,7 +62,8 @@ SOURCES += main.cpp \
settings/BabeSettings.cpp \
db/conthread.cpp \
services/web/babeit.cpp \
utils/babeconsole.cpp
utils/babeconsole.cpp \
services/local/youtubedl.cpp
RESOURCES += qml.qrc
......@@ -115,7 +117,8 @@ HEADERS += \
db/conthread.h \
services/web/babeit.h \
utils/babeconsole.h \
utils/singleton.h
utils/singleton.h \
services/local/youtubedl.h
#TAGLIB
......
......@@ -15,6 +15,7 @@
#include "utils/bae.h"
#include <QCommandLineParser>
#include "services/web/youtube.h"
int main(int argc, char *argv[])
{
......@@ -51,6 +52,9 @@ int main(int argc, char *argv[])
Player player;
Babe bae;
/* Services */
YouTube youtube;
QFontDatabase::addApplicationFont(":/utils/materialdesignicons-webfont.ttf");
qDebug()<<QQuickStyle::availableStyles();
......@@ -66,6 +70,7 @@ int main(int argc, char *argv[])
auto context = engine.rootContext();
context->setContextProperty("player", &player);
context->setContextProperty("bae", &bae);
context->setContextProperty("youtube", &youtube);
#ifdef Q_OS_ANDROID
KirigamiPlugin::getInstance().registerTypes();
......
......@@ -18,6 +18,7 @@ import "widgets/SearchView"
import "view_models"
import "view_models/BabeDialog"
import "services/web"
import "db/Queries.js" as Q
import "utils/Player.js" as Player
......@@ -106,6 +107,7 @@ Kirigami.ApplicationWindow
"playlists" : 3,
"babeit": 4,
"search" : 5,
"youtube" : 6
})
property bool mainlistEmpty : !mainPlaylist.table.count > 0
......@@ -756,6 +758,11 @@ Kirigami.ApplicationWindow
}
}
YouTube
{
id: youtubeView
}
}
}
}
......@@ -820,6 +827,17 @@ Kirigami.ApplicationWindow
}
/*CONNECTIONS*/
Connections
{
target: youtube
onQueryResultsReady:
{
if(res.length > 0)
youtubeView.web.url= res[0].url
}
}
Connections
{
target: player
......
......@@ -7,128 +7,128 @@
namespace PULPO
{
enum class SERVICES : uint8_t
{
LastFm,
Spotify,
iTunes,
MusicBrainz,
Genius,
LyricWikia,
Wikipedia,
WikiLyrics,
Deezer,
ALL,
NONE
};
enum class ONTOLOGY : uint8_t
{
ARTIST,
ALBUM,
TRACK,
NONE
};
enum class INFO : uint8_t
{
ARTWORK,
WIKI,
TAGS,
METADATA,
LYRICS,
ALL,
NONE
};
/*Generic context names. It's encouraged to use these instead of a unkown string*/
enum class CONTEXT : uint8_t
{
TRACK_STAT,
TRACK_NUMBER,
TRACK_TITLE,
TRACK_DATE,
TRACK_TEAM,
TRACK_AUTHOR,
TRACK_LANGUAGE,
TRACK_SIMILAR,
ALBUM_TEAM,
ALBUM_STAT,
ALBUM_TITLE,
ALBUM_DATE,
ALBUM_LANGUAGE,
ALBUM_SIMILAR,
ALBUM_LABEL,
ARTIST_STAT,
ARTIST_TITLE,
ARTIST_DATE,
ARTIST_LANGUAGE,
ARTIST_PLACE,
ARTIST_SIMILAR,
ARTIST_TEAM,
ARTIST_ALIAS,
ARTIST_GENDER,
GENRE,
TAG,
WIKI,
IMAGE,
LYRIC,
SOURCE
};
static const QMap<CONTEXT,QString> CONTEXT_MAP =
{
{CONTEXT::ALBUM_STAT, "album_stat"},
{CONTEXT::ALBUM_TITLE, "album_title"},
{CONTEXT::ALBUM_DATE, "album_date"},
{CONTEXT::ALBUM_LANGUAGE, "album_language"},
{CONTEXT::ALBUM_SIMILAR, "album_similar"},
{CONTEXT::ALBUM_LABEL, "album_label"},
{CONTEXT::ALBUM_TEAM, "album_team"},
{CONTEXT::ARTIST_STAT, "artist_stat"},
{CONTEXT::ARTIST_TITLE, "artist_title"},
{CONTEXT::ARTIST_DATE, "artist_date"},
{CONTEXT::ARTIST_LANGUAGE, "artist_language"},
{CONTEXT::ARTIST_PLACE, "artist_place"},
{CONTEXT::ARTIST_SIMILAR, "artist_similar"},
{CONTEXT::ARTIST_ALIAS, "artist_alias"},
{CONTEXT::ARTIST_GENDER, "artist_gender"},
{CONTEXT::ARTIST_TEAM, "artist_team"},
{CONTEXT::TRACK_STAT, "track_stat"},
{CONTEXT::TRACK_DATE, "track_date"},
{CONTEXT::TRACK_TITLE, "track_title"},
{CONTEXT::TRACK_NUMBER, "track_number"},
{CONTEXT::TRACK_TEAM, "track_team"},
{CONTEXT::TRACK_AUTHOR, "track_author"},
{CONTEXT::TRACK_LANGUAGE, "track_language"},
{CONTEXT::TRACK_SIMILAR, "track_similar"},
{CONTEXT::GENRE, "genre"},
{CONTEXT::TAG, "tag"},
{CONTEXT::WIKI, "wiki"},
{CONTEXT::IMAGE, "image"},
{CONTEXT::LYRIC, "lyric"},
{CONTEXT::SOURCE, "source"}
};
enum class RECURSIVE : bool
{
ON = true,
OFF = false
};
typedef QMap<CONTEXT, QVariant> VALUE;
typedef QMap<INFO, VALUE> INFO_K;
typedef QMap<ONTOLOGY, INFO_K> RESPONSE;
typedef QMap<ONTOLOGY, QList<INFO>> AVAILABLE;
enum class SERVICES : uint8_t
{
LastFm,
Spotify,
iTunes,
MusicBrainz,
Genius,
LyricWikia,
Wikipedia,
WikiLyrics,
Deezer,
ALL,
NONE
};
enum class ONTOLOGY : uint8_t
{
ARTIST,
ALBUM,
TRACK,
NONE
};
enum class INFO : uint8_t
{
ARTWORK,
WIKI,
TAGS,
METADATA,
LYRICS,
ALL,
NONE
};
/*Generic context names. It's encouraged to use these instead of a unkown string*/
enum class CONTEXT : uint8_t
{
TRACK_STAT,
TRACK_NUMBER,
TRACK_TITLE,
TRACK_DATE,
TRACK_TEAM,
TRACK_AUTHOR,
TRACK_LANGUAGE,
TRACK_SIMILAR,
ALBUM_TEAM,
ALBUM_STAT,
ALBUM_TITLE,
ALBUM_DATE,
ALBUM_LANGUAGE,
ALBUM_SIMILAR,
ALBUM_LABEL,
ARTIST_STAT,
ARTIST_TITLE,
ARTIST_DATE,
ARTIST_LANGUAGE,
ARTIST_PLACE,
ARTIST_SIMILAR,
ARTIST_TEAM,
ARTIST_ALIAS,
ARTIST_GENDER,
GENRE,
TAG,
WIKI,
IMAGE,
LYRIC,
SOURCE
};
static const QMap<CONTEXT,QString> CONTEXT_MAP =
{
{CONTEXT::ALBUM_STAT, "album_stat"},
{CONTEXT::ALBUM_TITLE, "album_title"},
{CONTEXT::ALBUM_DATE, "album_date"},
{CONTEXT::ALBUM_LANGUAGE, "album_language"},
{CONTEXT::ALBUM_SIMILAR, "album_similar"},
{CONTEXT::ALBUM_LABEL, "album_label"},
{CONTEXT::ALBUM_TEAM, "album_team"},
{CONTEXT::ARTIST_STAT, "artist_stat"},
{CONTEXT::ARTIST_TITLE, "artist_title"},
{CONTEXT::ARTIST_DATE, "artist_date"},
{CONTEXT::ARTIST_LANGUAGE, "artist_language"},
{CONTEXT::ARTIST_PLACE, "artist_place"},
{CONTEXT::ARTIST_SIMILAR, "artist_similar"},
{CONTEXT::ARTIST_ALIAS, "artist_alias"},
{CONTEXT::ARTIST_GENDER, "artist_gender"},
{CONTEXT::ARTIST_TEAM, "artist_team"},
{CONTEXT::TRACK_STAT, "track_stat"},
{CONTEXT::TRACK_DATE, "track_date"},
{CONTEXT::TRACK_TITLE, "track_title"},
{CONTEXT::TRACK_NUMBER, "track_number"},
{CONTEXT::TRACK_TEAM, "track_team"},
{CONTEXT::TRACK_AUTHOR, "track_author"},
{CONTEXT::TRACK_LANGUAGE, "track_language"},
{CONTEXT::TRACK_SIMILAR, "track_similar"},
{CONTEXT::GENRE, "genre"},
{CONTEXT::TAG, "tag"},
{CONTEXT::WIKI, "wiki"},
{CONTEXT::IMAGE, "image"},
{CONTEXT::LYRIC, "lyric"},
{CONTEXT::SOURCE, "source"}
};
enum class RECURSIVE : bool
{
ON = true,
OFF = false
};
typedef QMap<CONTEXT, QVariant> VALUE;
typedef QMap<INFO, VALUE> INFO_K;
typedef QMap<ONTOLOGY, INFO_K> RESPONSE;
typedef QMap<ONTOLOGY, QList<INFO>> AVAILABLE;
}
......
......@@ -234,8 +234,10 @@ QByteArray Pulpo::startConnection(const QString &url, const QMap<QString,QString
if(reply->bytesAvailable())
{
auto data = reply->readAll();
reply->deleteLater();
return reply->readAll();
return data;
}
}
......
......@@ -16,7 +16,6 @@
#include <QNetworkRequest>
#include <QJsonDocument>
#include <QVariantMap>
#include <QSqlQuery>
#include "../utils/bae.h"
#include "enums.h"
......@@ -25,49 +24,49 @@ using namespace PULPO;
class Pulpo : public QObject
{
Q_OBJECT
Q_OBJECT
public:
explicit Pulpo(const BAE::DB &song, QObject *parent = nullptr);
explicit Pulpo(QObject *parent = nullptr);
~Pulpo();
public:
explicit Pulpo(const BAE::DB &song, QObject *parent = nullptr);
explicit Pulpo(QObject *parent = nullptr);
~Pulpo();
bool feed(const BAE::DB &song, const PULPO::RECURSIVE &recursive = PULPO::RECURSIVE::ON );
void registerServices(const QList<PULPO::SERVICES> &services);
void setInfo(const PULPO::INFO &info);
void setOntology(const PULPO::ONTOLOGY &ontology);
PULPO::ONTOLOGY getOntology();
void setRecursive(const PULPO::RECURSIVE &state);
bool feed(const BAE::DB &song, const PULPO::RECURSIVE &recursive = PULPO::RECURSIVE::ON );
void registerServices(const QList<PULPO::SERVICES> &services);
void setInfo(const PULPO::INFO &info);
void setOntology(const PULPO::ONTOLOGY &ontology);
PULPO::ONTOLOGY getOntology();
void setRecursive(const PULPO::RECURSIVE &state);
private:
void initServices();
PULPO::RECURSIVE recursive = PULPO::RECURSIVE::ON;
QList<SERVICES> registeredServices = {};
private:
void initServices();
PULPO::RECURSIVE recursive = PULPO::RECURSIVE::ON;
QList<SERVICES> registeredServices = {};
void passSignal(const BAE::DB &track, const PULPO::RESPONSE &response);
void passSignal(const BAE::DB &track, const PULPO::RESPONSE &response);
protected:
QByteArray array;
BAE::DB track;
PULPO::INFO info = INFO::NONE;
PULPO::ONTOLOGY ontology = ONTOLOGY::NONE;
PULPO::AVAILABLE availableInfo;
protected:
QByteArray array;
BAE::DB track;
PULPO::INFO info = INFO::NONE;
PULPO::ONTOLOGY ontology = ONTOLOGY::NONE;
PULPO::AVAILABLE availableInfo;
PULPO::RESPONSE packResponse(const PULPO::ONTOLOGY ontology, const PULPO::INFO &infoKey, const PULPO::CONTEXT &contextName, const QVariant &value);
PULPO::RESPONSE packResponse(const PULPO::ONTOLOGY ontology, const PULPO::INFO &infoKey, const PULPO::VALUE &map);
PULPO::RESPONSE packResponse(const PULPO::ONTOLOGY ontology, const PULPO::INFO &infoKey, const PULPO::CONTEXT &contextName, const QVariant &value);
PULPO::RESPONSE packResponse(const PULPO::ONTOLOGY ontology, const PULPO::INFO &infoKey, const PULPO::VALUE &map);
QByteArray startConnection(const QString &url, const QMap<QString, QString> &headers = {});
bool parseArray();
QByteArray startConnection(const QString &url, const QMap<QString, QString> &headers = {});
bool parseArray();
/* expected methods to be overrided by services */
bool setUpService(const PULPO::ONTOLOGY &ontology, const PULPO::INFO &info);
virtual bool parseArtist() {return false;}
virtual bool parseAlbum() {return false;}
virtual bool parseTrack() {return false;}
/* expected methods to be overrided by services */
bool setUpService(const PULPO::ONTOLOGY &ontology, const PULPO::INFO &info);
virtual bool parseArtist() {return false;}
virtual bool parseAlbum() {return false;}
virtual bool parseTrack() {return false;}
signals:
void infoReady(BAE::DB track, PULPO::RESPONSE response);
void serviceFail(const QString &message);
signals:
void infoReady(BAE::DB track, PULPO::RESPONSE response);
void serviceFail(const QString &message);
};
#endif // ARTWORK_H
......@@ -52,7 +52,7 @@ bool genius::setUpService(const PULPO::ONTOLOGY &ontology, const PULPO::INFO &in
qDebug()<< "[genius service]: "<< newUrl;
this->array = this->startConnection( newUrl,{{"Authorization", this->KEY}} );
this->array = this->startConnection(newUrl,{{"Authorization", this->KEY}} );
if(this->array.isEmpty()) return false;
return this->parseArray();
......
......@@ -72,5 +72,6 @@
<file>widgets/SettingsView/BabeConsole.qml</file>
<file>widgets/SearchView/SearchTable.qml</file>
<file>widgets/SearchView/SearchSuggestions.qml</file>
<file>services/web/YouTube.qml</file>
</qresource>
</RCC>
/*
Babe - tiny music player
Copyright (C) 2017 Camilo Higuita
This program 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.
This program 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 this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "youtubedl.h"
#include "../../pulpo/pulpo.h"
#include "../../db/collectionDB.h"
#include "../../utils/babeconsole.h"
#if (defined (Q_OS_LINUX) && !defined (Q_OS_ANDROID))
#include "kde/notify.h"
#endif
using namespace BAE;
youtubedl::youtubedl(QObject *parent) : QObject(parent)
{
#if (defined (Q_OS_LINUX) && !defined (Q_OS_ANDROID))
this->nof = new Notify(this);
#endif
}
youtubedl::~youtubedl(){}
void youtubedl::fetch(const QString &json)
{
QJsonParseError jsonParseError;
auto jsonResponse = QJsonDocument::fromJson(json.toUtf8(), &jsonParseError);
if (jsonParseError.error != QJsonParseError::NoError) return;
if (!jsonResponse.isObject()) return;
QJsonObject mainJsonObject(jsonResponse.object());
auto data = mainJsonObject.toVariantMap();
auto id = data.value("id").toString().trimmed();
auto title = data.value("title").toString().trimmed();
auto artist = data.value("artist").toString().trimmed();
auto album = data.value("album").toString().trimmed();
auto playlist = data.value("playlist").toString().trimmed();
auto page = data.value("page").toString().replace('"',"").trimmed();
bDebug::Instance()->msg("Fetching from Youtube: "+id+" "+title+" "+artist);
DB infoMap;
infoMap.insert(KEY::TITLE, title);
infoMap.insert(KEY::ARTIST, artist);
infoMap.insert(KEY::ALBUM, album);
infoMap.insert(KEY::URL, page);
infoMap.insert(KEY::ID, id);
infoMap.insert(KEY::PLAYLIST, playlist);
if(!this->ids.contains(infoMap[KEY::ID]))
{
this->ids << infoMap[KEY::ID];
auto process = new QProcess(this);
process->setWorkingDirectory(YoutubeCachePath);
//connect(process, SIGNAL(readyReadStandardOutput()), this, SLOT(processFinished()));
//connect(process, SIGNAL(finished(int)), this, SLOT(processFinished_totally(int)));
connect(process, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
[=](int exitCode, QProcess::ExitStatus exitStatus)
{
// qDebug()<<"processFinished_totally"<<exitCode<<exitStatus;
processFinished_totally(exitCode, infoMap, exitStatus);
process->deleteLater();
});
#if (defined (Q_OS_LINUX) && !defined (Q_OS_ANDROID))
this->nof->notify("Song received!", infoMap[KEY::TITLE]+ " - "+ infoMap[KEY::ARTIST]+".\nWait a sec while the track is added to your collection :)");
#endif
auto command = ydl;
command = command.replace("$$$",infoMap[KEY::ID])+" "+infoMap[KEY::ID];
bDebug::Instance()->msg(command);
process->start(command);
}
}
void youtubedl::processFinished_totally(const int &state,const DB &info,const QProcess::ExitStatus &exitStatus)
{
auto track = info;
auto doneId = track[KEY::ID];
auto file = YoutubeCachePath+track[KEY::ID]+".m4a";
if(!BAE::fileExists(file)) return;
ids.removeAll(doneId);
track.insert(KEY::URL,file);
bDebug::Instance()->msg("Finished collection track with youtube-dl");
// qDebug()<<track[KEY::ID]<<track[KEY::TITLE]<<track[KEY::ARTIST]<<track[KEY::PLAYLIST]<<track[KEY::URL];
/*here get metadata*/
TagInfo tag;
if(exitStatus == QProcess::NormalExit)
{
if(BAE::fileExists(file))
{
tag.feed(file);
tag.setArtist(track[KEY::ARTIST]);
tag.setTitle(track[KEY::TITLE]);
tag.setAlbum(track[KEY::ALBUM]);
tag.setComment(track[KEY::URL]);
bDebug::Instance()->msg("Trying to collect metadata of downloaded track");
Pulpo pulpo;
pulpo.registerServices({PULPO::SERVICES::LastFm, PULPO::SERVICES::Spotify});
pulpo.setOntology(PULPO::ONTOLOGY::TRACK);
pulpo.setInfo(PULPO::INFO::METADATA);
QEventLoop loop;
QTimer timer;
connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
timer.setSingleShot(true);
timer.setInterval(1000);
connect(&pulpo, &Pulpo::infoReady, [&loop](const BAE::DB &track, const PULPO::RESPONSE &res)
{
bDebug::Instance()->msg("Setting collected track metadata");
if(!res[PULPO::ONTOLOGY::TRACK][PULPO::INFO::METADATA].isEmpty())
{
bDebug::Instance()->msg(res[PULPO::ONTOLOGY::TRACK][PULPO::INFO::METADATA][PULPO::CONTEXT::ALBUM_TITLE].toString());
bDebug::Instance()->msg(res[PULPO::ONTOLOGY::TRACK][PULPO::INFO::METADATA][PULPO::CONTEXT::TRACK_NUMBER].toString());
TagInfo tag;
tag.feed(track[KEY::URL]);
auto albumRes = res[PULPO::ONTOLOGY::TRACK][PULPO::INFO::METADATA][PULPO::CONTEXT::ALBUM_TITLE].toString();
if(!albumRes.isEmpty() && albumRes != BAE::SLANG[W::UNKNOWN])
tag.setAlbum(res[PULPO::ONTOLOGY::TRACK][PULPO::INFO::METADATA][PULPO::CONTEXT::ALBUM_TITLE].toString());
else tag.setAlbum(track[KEY::TITLE]);
if(!res[PULPO::ONTOLOGY::TRACK][PULPO::INFO::METADATA][PULPO::CONTEXT::TRACK_NUMBER].toString().isEmpty())
tag.setTrack(res[PULPO::ONTOLOGY::TRACK][PULPO::INFO::METADATA][PULPO::CONTEXT::TRACK_NUMBER].toInt());
}
loop.quit();
});
pulpo.feed(track, PULPO::RECURSIVE::OFF);
timer.start();
loop.exec();
timer.stop();
bDebug::Instance()->msg("Process finished totally for "+QString(state)+" "+doneId+" "+QString(exitStatus));
bDebug::Instance()->msg("Need to delete the id "+ doneId);
bDebug::Instance()->msg("Ids left to process: " + this->ids.join(","));
}
}
tag.feed(file);
auto album = BAE::fixString(tag.getAlbum());
auto trackNum = tag.getTrack();
auto title = BAE::fixString(tag.getTitle()); /* to fix*/
auto artist = BAE::fixString(tag.getArtist());
auto genre = tag.getGenre();
auto sourceUrl = QFileInfo(file).dir().path();
auto duration = tag.getDuration();
auto year = tag.getYear();
BAE::DB trackMap =
{
{BAE::KEY::URL,file},
{BAE::KEY::TRACK,QString::number(trackNum)},
{BAE::KEY::TITLE,title},
{BAE::KEY::ARTIST,artist},
{BAE::KEY::ALBUM,album},
{BAE::KEY::DURATION,QString::number(duration)},
{BAE::KEY::GENRE,genre},
{BAE::KEY::SOURCES_URL,sourceUrl},
{BAE::KEY::BABE, file.startsWith(BAE::YoutubeCachePath)?"1":"0"},
{BAE::KEY::RELEASE_DATE,QString::number(year)}
};
CollectionDB con(nullptr);
con.addTrack(trackMap);
con.trackPlaylist({file}, track[KEY::PLAYLIST]);
if(this->ids.isEmpty()) emit this->done();
}
void youtubedl::processFinished()
{
/* QByteArray processOutput;
processOutput = process->readAllStandardOutput();
if (!QString(processOutput).isEmpty())
qDebug() << "Output: " << QString(processOutput);*/
}
#ifndef YOUTUBEDL_H
#define YOUTUBEDL_H
#include <QObject>
#include <QWidget>
#include <QProcess>
#include <QByteArray>
#include <QMovie>
#include <QDebug>
#include <QDirIterator>
#include <fstream>
#include <iostream>
#include "../../utils/bae.h"
#include "../local/taginfo.h"
#if (defined (Q_OS_LINUX) && !defined (Q_OS_ANDROID))
class Notify;
#endif
class youtubedl : public QObject
{
Q_OBJECT
public:
explicit youtubedl(QObject *parent = nullptr);
~youtubedl();
void fetch(const QString &json);
QStringList ids;
private slots:
void processFinished();
void processFinished_totally(const int &state, const BAE::DB &info, const QProcess::ExitStatus &exitStatus);
private:
const QString ydl="youtube-dl -f m4a --youtube-skip-dash-manifest -o \"$$$.%(ext)s\"";
#if (defined (Q_OS_LINUX) && !defined (Q_OS_ANDROID))
Notify *nof;
#endif
signals:
void done();
};
#endif // YOUTUBEDL_H
import QtQuick 2.9
import QtWebKit 3.0
import QtQuick.Controls 2.2