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