1 /*
2 * Cantata
3 *
4 * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5 *
6 * ----
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; see the file COPYING. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24 #include "digitallyimported.h"
25 #include "support/configuration.h"
26 #include "network/networkaccessmanager.h"
27 #include "support/globalstatic.h"
28 #include <QNetworkRequest>
29 #include <QJsonDocument>
30 #include <QTime>
31 #include <QTimer>
32
33 static const char * constDiGroup="DigitallyImported";
34 static const QStringList constPremiumValues=QStringList() << QLatin1String("premium_high") << QLatin1String("premium_medium") << QLatin1String("premium");
35 static const QUrl constAuthUrl(QLatin1String("http://api.audioaddict.com/v1/di/members/authenticate"));
36 const QString DigitallyImported::constApiUserName=QLatin1String("ephemeron");
37 const QString DigitallyImported::constApiPassword=QLatin1String("dayeiph0ne@pp");
38 const QString DigitallyImported::constPublicValue=QLatin1String("public3");
39
GLOBAL_STATIC(DigitallyImported,instance)40 GLOBAL_STATIC(DigitallyImported, instance)
41
42 DigitallyImported::DigitallyImported()
43 : job(nullptr)
44 , streamType(0)
45 , timer(nullptr)
46 {
47 load();
48 }
49
~DigitallyImported()50 DigitallyImported::~DigitallyImported()
51 {
52 }
53
login()54 void DigitallyImported::login()
55 {
56 if (job) {
57 job->deleteLater();
58 job=nullptr;
59 }
60 QNetworkRequest req(constAuthUrl);
61 addAuthHeader(req);
62 job=NetworkAccessManager::self()->postFormData(req, "username="+QUrl::toPercentEncoding(userName)+"&password="+QUrl::toPercentEncoding(password));
63 connect(job, SIGNAL(finished()), SLOT(loginResponse()));
64 }
65
logout()66 void DigitallyImported::logout()
67 {
68 if (job) {
69 job->deleteLater();
70 job=nullptr;
71 }
72 listenHash=QString();
73 expires=QDateTime();
74 controlTimer();
75 }
76
addAuthHeader(QNetworkRequest & req) const77 void DigitallyImported::addAuthHeader(QNetworkRequest &req) const
78 {
79 req.setRawHeader("Authorization", "Basic "+QString("%1:%2").arg(constApiUserName, constApiPassword).toLatin1().toBase64());
80 }
81
load()82 void DigitallyImported::load()
83 {
84 Configuration cfg(constDiGroup);
85
86 userName=cfg.get("userName", userName);
87 password=cfg.get("password", password);
88 listenHash=cfg.get("listenHash", listenHash);
89 streamType=cfg.get("streamType", streamType);
90 QString ex=cfg.get("expires", QString());
91
92 status=tr("Not logged in");
93 if (ex.isEmpty()) {
94 listenHash=QString();
95 } else {
96 expires=QDateTime::fromString(ex, Qt::ISODate);
97 // If we have expired, or are about to expire in 5 minutes, then clear the hash...
98 if (QDateTime::currentDateTime().secsTo(expires)<(5*60)) {
99 listenHash=QString();
100 } else if (!listenHash.isEmpty()) {
101 status=tr("Logged in");
102 }
103 }
104 controlTimer();
105 }
106
save()107 void DigitallyImported::save()
108 {
109 Configuration cfg(constDiGroup);
110
111 cfg.set("userName", userName);
112 cfg.set("password", password);
113 cfg.set("listenHash", listenHash);
114 cfg.set("streamType", streamType);
115 cfg.set("expires", expires.toString(Qt::ISODate));
116 emit updated();
117 }
118
isDiUrl(const QString & u) const119 bool DigitallyImported::isDiUrl(const QString &u) const
120 {
121 if (!u.startsWith(QLatin1String("http://"))) {
122 return false;
123 }
124 QUrl url(u);
125 if (!url.host().startsWith(QLatin1String("listen."))) {
126 return false;
127 }
128 QStringList pathParts=url.path().split(QLatin1Char('/'), QString::SkipEmptyParts);
129 if (2!=pathParts.count()) {
130 return false;
131 }
132 return pathParts.at(0)==constPublicValue;
133 }
134
modifyUrl(const QString & u) const135 QString DigitallyImported::modifyUrl(const QString &u) const
136 {
137 if (listenHash.isEmpty()) {
138 return u;
139 }
140 QString premValue=constPremiumValues.at(streamType>0 && streamType<constPremiumValues.count() ? streamType : 0);
141 QString url=u;
142 return url.replace(constPublicValue, premValue)+QLatin1String("?hash=")+listenHash;
143 }
144
loginResponse()145 void DigitallyImported::loginResponse()
146 {
147 QNetworkReply *reply=dynamic_cast<QNetworkReply *>(sender());
148
149 if (!reply) {
150 return;
151 }
152 reply->deleteLater();
153
154 if (reply!=job) {
155 return;
156 }
157 job=nullptr;
158
159 status=listenHash=QString();
160 const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
161 if (403==httpStatus) {
162 status=reply->readAll();
163 emit loginStatus(false, status);
164 return;
165 } else if (200!=httpStatus) {
166 status=tr("Unknown error");
167 emit loginStatus(false, status);
168 return;
169 }
170
171 QVariantMap data=QJsonDocument::fromJson(reply->readAll()).toVariant().toMap();
172
173 if (!data.contains("subscriptions")) {
174 status=tr("No subscriptions");
175 emit loginStatus(false, status);
176 return;
177 }
178
179 QVariantList subscriptions = data.value("subscriptions", QVariantList()).toList();
180 if (subscriptions.isEmpty() || QLatin1String("active")!=subscriptions[0].toMap().value("status").toString()) {
181 status=tr("You do not have an active subscription");
182 emit loginStatus(false, status);
183 return;
184 }
185
186 if (!subscriptions[0].toMap().contains("expires_on") || !data.contains("listen_key")) {
187 status=tr("Unknown error");
188 emit loginStatus(false, status);
189 return;
190 }
191
192 QDateTime ex = QDateTime::fromString(subscriptions[0].toMap()["expires_on"].toString(), Qt::ISODate);
193 QString lh = data["listen_key"].toString();
194
195 if (ex!=expires || lh!=listenHash) {
196 expires=ex;
197 listenHash=lh;
198 save();
199 }
200 status=tr("Logged in (expiry:%1)").arg(expires.toString(Qt::ISODate));
201 controlTimer();
202 emit loginStatus(true, status);
203 }
204
timeout()205 void DigitallyImported::timeout()
206 {
207 listenHash=QString();
208 emit loginStatus(false, tr("Session expired"));
209 }
210
controlTimer()211 void DigitallyImported::controlTimer()
212 {
213 if (!expires.isValid() || QDateTime::currentDateTime().secsTo(expires)<15) {
214 if (timer && timer->isActive()) {
215 if (!listenHash.isEmpty()) {
216 timeout();
217 }
218 timer->stop();
219 }
220 } else {
221 if (!timer) {
222 timer=new QTimer(this);
223 connect(timer, SIGNAL(timeout()), SLOT(timeout()));
224 }
225 int secsTo=QDateTime::currentDateTime().secsTo(expires);
226
227 if (secsTo>4) {
228 timer->start((secsTo-3)*1000);
229 } else {
230 timeout();
231 }
232 }
233 }
234
235 #include "moc_digitallyimported.cpp"
236