1
/***************************************************************************ß
2
* Copyright (C) 2009 by Kai Dombrowe <just89@gmx.de> *
4
* This program is free software; you can redistribute it and/or modify *
5
* it under the terms of the GNU General Public License as published by *
6
* the Free Software Foundation; either version 2 of the License, or *
7
* (at your option) any later version. *
9
* This program is distributed in the hope that it will be useful, *
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12
* GNU General Public License for more details. *
14
* You should have received a copy of the GNU General Public License *
15
* along with this program; if not, write to the *
16
* Free Software Foundation, Inc., *
17
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . *
18
***************************************************************************/
21
#include "youtubeservice.h"
26
#include <kmimetype.h>
27
#include <klocalizedstring.h>
30
#include <QtCore/QFile>
31
#include <QtXml/QXmlStreamReader>
32
#include <QtCore/QDateTime>
35
#define DEV_KEY "AI39si4PPp_RmxGSVs4cHH93rcG2e9vSRQU1vC0L3sfuy_ZHmtaAWZOdvSfBjmow3YSZfrerx"\
36
"jhsZGX0brUrdSLr5qvNchxeiQ"
38
YouTubeService::YouTubeService(QObject *parent)
39
: KYouBlip::Service(parent)
42
m_authenticated = false;
47
YouTubeService::~YouTubeService()
53
bool YouTubeService::isAuthenticated(const QString &account) const
56
return !m_token[account].isEmpty();
61
QString YouTubeService::authenticate(const QString &account, const QString &password)
64
if (account.isEmpty() || password.isEmpty()) {
65
return "Error: "+i18n("No account/password specified.");
68
const KUrl url("https://www.google.com/youtube/accounts/ClientLogin");
71
meta.insert("content-type", "Content-Type: application/x-www-form-urlencoded");
72
meta.insert("errorPage", "false");
74
QByteArray postData = "Email="+account.toLatin1()+"&Passwd="+password.toLatin1()+"&service="\
75
"youtube&source=RecordItNow";
77
const QString id = Service::getUniqueId();
78
m_accountIds[id] = account;
80
m_jobs[post(url, meta, postData, true)] = qMakePair(AuthJob, account);
87
QString YouTubeService::upload(const YouTubeVideo *video, const QString &account)
91
if (!isAuthenticated(account)) {
92
errorString = i18n("Please authenticate first!");
95
const QString videoFile = video->file();
96
const QString title = video->title();
97
const QString description = video->description();
98
const QString category = video->getCategory();
99
const QString tags = video->keywords().join(", ");
101
int index = videoFile.lastIndexOf('/');
102
index != -1 ? index++ : index = 0;
103
QString fileName = videoFile.mid(index);
105
if (!QFile::exists(videoFile)) {
106
errorString = i18n("No such file: %1.", videoFile);
109
if (title.length() > 60 || title.toLatin1().size() > 100) {
110
errorString = i18n("Title too long.");
113
if (description.length() > 5000) {
114
errorString = i18n("Description too long.");
117
if (tags.length() > 120) {
118
errorString = i18n("Too long tags.");
121
if (title.isEmpty()) {
122
errorString = i18n("No title specified.");
125
if (description.isEmpty()) {
126
errorString = i18n("No description specified.");
129
if (tags.isEmpty()) {
130
errorString = i18n("No tags specified.");
133
if (category == "-5") {
134
kDebug() << "category" << category;
135
errorString = i18n("Invalid category.");
138
foreach (const QString &keyword, tags.split(',')) {
139
if (keyword.length() > 25) {
140
errorString = i18n("Tag \"%1\" is too long.", keyword);
142
} else if (keyword.length() < 2) {
143
errorString = i18n("Tag \"%1\" is too short.", keyword);
148
if (!errorString.isEmpty()) {
149
return "Error: "+errorString;
152
const KUrl url("http://uploads.gdata.youtube.com/feeds/api/users/"+account+"/uploads");
154
QByteArray CRLF = "\r\n";
155
QByteArray BOUNDARY = "f93dcbA3";
157
QFile file(videoFile);
158
if (!file.open(QIODevice::ReadOnly)) {
159
emit error(i18n("Cannot open video!"), account);
162
QByteArray videoData = file.readAll();
165
KMimeType::Ptr type = KMimeType::findByUrl(KUrl(videoFile));
166
QByteArray mime = type->name().toLatin1();
167
if (mime.isEmpty()) {
168
mime = "application/octet-stream";
171
QString XML = "<?xml version=\"1.0\"?>"+CRLF+\
172
"<entry xmlns=\"http://www.w3.org/2005/Atom\""+CRLF+\
173
"xmlns:media=\"http://search.yahoo.com/mrss/\""+CRLF+\
174
"xmlns:yt=\"http://gdata.youtube.com/schemas/2007\">"+CRLF+\
175
"<media:group>"+CRLF+\
176
"<media:title type=\"plain\">%1</media:title>"+CRLF+\
177
"<media:description type=\"plain\">"+CRLF+\
179
"</media:description>"+CRLF+\
180
"<media:category"+CRLF+\
181
"scheme=\"http://gdata.youtube.com/schemas/2007/categories.cat\">%3"+CRLF+\
182
"</media:category>"+CRLF+\
183
"<media:keywords>%4</media:keywords>"+CRLF+\
184
"</media:group>"+CRLF+\
187
XML = XML.arg(title).arg(description).arg(category).arg(tags);
190
postData.append("--"+BOUNDARY);
191
postData.append(CRLF);
192
postData.append("Content-Type: application/atom+xml; charset=UTF-8");
193
postData.append(CRLF);
194
postData.append(CRLF);
195
postData.append(XML.toLatin1());
196
postData.append(CRLF);
197
postData.append("--"+BOUNDARY);
198
postData.append(CRLF);
199
postData.append("Content-Type: "+mime);
200
postData.append(CRLF);
201
postData.append("Content-Transfer-Encoding: binary");
202
postData.append(CRLF);
203
postData.append(CRLF);
204
postData.append(videoData);
205
postData.append(CRLF);
206
postData.append("--"+BOUNDARY+"--");
207
postData.append(CRLF);
210
const QString id = Service::getUniqueId();
211
m_accountIds[id] = account;
213
QHash<QString, QString> header;
214
header["Authorization"] = "GoogleLogin auth="+m_token[account].toLatin1();
215
header["GData-Version"] = '2';
216
header["X-GData-Key"] = "key="+QString(DEV_KEY).toLatin1();
217
header["Connection"] = "close";
218
header["Slug"] = fileName.toLatin1();
219
header["Content-Type"] = "multipart/related; boundary=\""+BOUNDARY+"\"";
220
header["Content-Length"] = QString::number(postData.size()).toLatin1();
222
InfoJob *job = post(url, header, postData);
223
m_jobs[job] = qMakePair(UploadJob, id);
224
job->setSource(video->file());
231
QString YouTubeService::search(const QString &key, const QString &author, const int &start,
243
const QString id = Service::getUniqueId();
245
KUrl url("http://gdata.youtube.com/feeds/api/videos");
246
url.addQueryItem("q", key);
247
url.addQueryItem("start-index", QString::number(start));
248
url.addQueryItem("max-results", QString::number(max));
250
if (!author.isEmpty()) {
251
url.addQueryItem("author", author);
254
m_jobs[get(url, KIO::NoReload, true)] = qMakePair(SearchJob, id);
260
QString YouTubeService::getFavorites(const QString &user, const int &max)
263
const QString id = Service::getUniqueId();
264
KUrl url("http://gdata.youtube.com/feeds/api/users/"+user+"/favorites?v=2");
265
url.addQueryItem("max-results", QString::number(max));
266
m_jobs[get(url, KIO::NoReload, true)] = qMakePair(FavoritesJob, id);
272
void YouTubeService::cancelUpload()
275
QHashIterator<KJob*, JobData> it(m_jobs);
276
while (it.hasNext()) {
286
void YouTubeService::cancelUpload(const QString &id)
289
QHashIterator<KJob*, JobData> it(m_jobs);
290
while (it.hasNext()) {
292
if (it.key() && it.value().second == id) {
300
void YouTubeService::jobFinished(KJob *job, const QByteArray &data)
303
JobData jData = m_jobs[job];
304
QString id = jData.second;
305
const JobType type = jData.first;
306
const int ret = job->error();
310
kDebug() << "job finished:" << type << id << data;
312
QString response = data.trimmed();
317
const QString key = id;
318
id = m_accountIds.key(key);
319
m_accountIds.remove(key);
321
if (response.startsWith(QLatin1String("Auth="))) {
322
response.remove("Auth=");
324
const QStringList lines = response.split('\n');
325
QString user = lines.last();
326
user.remove(QRegExp(".*="));
327
m_token[user] = lines.first();
328
emit authenticated(id);
329
} else if (ret == KIO::ERR_USER_CANCELED) {
332
emit error(i18n("Authentication failed!"), id);
338
const QString key = id;
339
id = m_accountIds.key(key);
340
m_accountIds.remove(key);
342
YouTubeVideo *video = 0;
343
QXmlStreamReader reader(response);
344
while (!reader.atEnd()) {
346
if (reader.isStartElement() && reader.name() == "entry") {
347
video = readEntry(&reader);
350
if ((reader.hasError() || !video) && ret != KIO::ERR_USER_CANCELED) {
351
emit error(i18nc("%1 = error", "Upload failed!\n"
352
"Response: %1", response), id);
355
emit uploadFinished(video, id);
360
QList<YouTubeVideo*> videoList;
361
QXmlStreamReader reader(data);
362
while (!reader.atEnd()) {
364
if (reader.isStartElement()) {
365
if (reader.name() == "feed") {
366
while (!reader.atEnd()) {
368
if (reader.isStartElement() && reader.name() == "entry") {
369
videoList.append(readEntry(&reader));
375
if (type == SearchJob) {
376
emit searchFinished(videoList, id);
378
emit favoritesFinished(videoList, id);
388
YouTubeVideo *YouTubeService::readEntry(QXmlStreamReader *reader)
391
YouTubeVideo *video = new YouTubeVideo(this);
392
while (!reader->atEnd()) {
395
if (reader->isEndElement() && reader->name() == "entry") {
399
if (reader->isStartElement()) {
400
if (reader->name() == "link"
401
&& reader->attributes().value("rel").toString() == "alternate"
402
&& reader->attributes().value("type").toString() == "text/html") {
403
QString webpage = reader->attributes().value("href").toString();
404
video->setUrl(KUrl(webpage));
405
} else if (reader->name() == "author") {
407
if (reader->name() == "name") {
408
video->setAuthor(reader->readElementText());
410
} else if (reader->name() == "published") {
411
video->setPublished(QDateTime::fromString(reader->readElementText(), Qt::ISODate));
412
} else if (reader->namespaceUri() == "http://gdata.youtube.com/schemas/2007"
413
&& reader->name() == "statistics") {
414
video->setViewCount(reader->attributes().value("viewCount").toString().toInt());
415
video->setFavoriteCount(reader->attributes().value("favoriteCount").toString().toInt());
417
else if (reader->namespaceUri() == "http://search.yahoo.com/mrss/"
418
&& reader->name() == "group") {
421
while (!reader->atEnd()) {
423
if (reader->isEndElement() && reader->name() == "group") {
426
if (reader->isStartElement()) {
427
if (reader->name() == "thumbnail") {
428
video->setThumbnailUrl(KUrl(reader->attributes().value("url").toString()));
429
} else if (reader->name() == "title") {
430
QString title = reader->readElementText();
431
video->setTitle(title);
432
} else if (reader->name() == "description") {
433
QString desc = reader->readElementText();
434
video->setDescription(desc);
435
} else if (reader->name() == "duration") {
436
QString duration = reader->attributes().value("seconds").toString();
437
video->setDuration(duration.toInt());
438
} else if (reader->name() == "keywords") {
439
video->setKeywords(reader->readElementText());
440
} else if (reader->name() == "category") {
441
video->setCategory(reader->readElementText());
445
} else if (reader->name() == "rating") {
446
video->setRating(reader->attributes().value("average").toString().toDouble());
447
video->setRaters(reader->attributes().value("numRaters").toString().toInt());
456
#include "youtubeservice.moc"