UpnpDatabasePlugin.cxx 16.9 KB
Newer Older
1
/*
2
 * Copyright 2003-2016 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 * http://www.musicpd.org
 *
 * 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 2 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 "config.h"
#include "UpnpDatabasePlugin.hxx"
22 23
#include "Directory.hxx"
#include "Tags.hxx"
24
#include "lib/upnp/ClientInit.hxx"
25 26 27
#include "lib/upnp/Discovery.hxx"
#include "lib/upnp/ContentDirectoryService.hxx"
#include "lib/upnp/Util.hxx"
28
#include "db/Interface.hxx"
Max Kellermann's avatar
Max Kellermann committed
29 30 31 32 33
#include "db/DatabasePlugin.hxx"
#include "db/Selection.hxx"
#include "db/DatabaseError.hxx"
#include "db/LightDirectory.hxx"
#include "db/LightSong.hxx"
34
#include "db/Stats.hxx"
35
#include "config/Block.hxx"
36 37
#include "tag/TagBuilder.hxx"
#include "tag/TagTable.hxx"
38
#include "fs/Traits.hxx"
39 40 41 42 43 44 45 46 47 48 49 50
#include "Log.hxx"
#include "SongFilter.hxx"

#include <string>
#include <vector>
#include <set>

#include <assert.h>
#include <string.h>

static const char *const rootid = "0";

51
class UpnpSong : public LightSong {
52
	std::string uri2, real_uri2;
53 54 55 56

	Tag tag2;

public:
57 58 59
	UpnpSong(UPnPDirObject &&object, std::string &&_uri)
		:uri2(std::move(_uri)),
		 real_uri2(std::move(object.url)),
60
		 tag2(std::move(object.tag)) {
61 62
		directory = nullptr;
		uri = uri2.c_str();
63
		real_uri = real_uri2.c_str();
64 65
		tag = &tag2;
		mtime = 0;
66
		start_time = end_time = SongTime::zero();
67 68 69
	}
};

70
class UpnpDatabase : public Database {
71
	UpnpClient_Handle handle;
72
	UPnPDeviceDirectory *discovery;
73 74

public:
75 76
	UpnpDatabase():Database(upnp_db_plugin) {}

77
	static Database *Create(EventLoop &loop, DatabaseListener &listener,
78
				const ConfigBlock &block);
79

80
	virtual void Open() override;
81
	virtual void Close() override;
82
	virtual const LightSong *GetSong(const char *uri_utf8) const override;
83
	void ReturnSong(const LightSong *song) const override;
84

85 86 87 88
	void Visit(const DatabaseSelection &selection,
		   VisitDirectory visit_directory,
		   VisitSong visit_song,
		   VisitPlaylist visit_playlist) const override;
89

90 91 92 93 94
	void VisitUniqueTags(const DatabaseSelection &selection,
			     TagType tag_type, tag_mask_t group_mask,
			     VisitTag visit_tag) const override;

	DatabaseStats GetStats(const DatabaseSelection &selection) const override;
95

96 97 98
	time_t GetUpdateStamp() const override {
		return 0;
	}
99 100

private:
101
	void VisitServer(const ContentDirectoryService &server,
102
			 const std::list<std::string> &vpath,
103 104 105
			 const DatabaseSelection &selection,
			 VisitDirectory visit_directory,
			 VisitSong visit_song,
106
			 VisitPlaylist visit_playlist) const;
107 108 109 110 111

	/**
	 * Run an UPnP search according to MPD parameters, and
	 * visit_song the results.
	 */
112
	void SearchSongs(const ContentDirectoryService &server,
113 114
			 const char *objid,
			 const DatabaseSelection &selection,
115
			 VisitSong visit_song) const;
116

117 118 119
	UPnPDirContent SearchSongs(const ContentDirectoryService &server,
				   const char *objid,
				   const DatabaseSelection &selection) const;
120

121 122
	UPnPDirObject Namei(const ContentDirectoryService &server,
			    const std::list<std::string> &vpath) const;
123 124 125 126

	/**
	 * Take server and objid, return metadata.
	 */
127 128
	UPnPDirObject ReadNode(const ContentDirectoryService &server,
			       const char *objid) const;
129 130 131 132 133 134

	/**
	 * Get the path for an object Id. This works much like pwd,
	 * except easier cause our inodes have a parent id. Not used
	 * any more actually (see comments in SearchSongs).
	 */
135 136
	std::string BuildPath(const ContentDirectoryService &server,
			      const UPnPDirObject& dirent) const;
137 138 139
};

Database *
140 141
UpnpDatabase::Create(gcc_unused EventLoop &loop,
		     gcc_unused DatabaseListener &listener,
142
		     const ConfigBlock &)
143
{
144
	return new UpnpDatabase();
145 146
}

147 148
void
UpnpDatabase::Open()
149
{
150
	UpnpClientGlobalInit(handle);
151

152
	discovery = new UPnPDeviceDirectory(handle);
153 154 155
	try {
		discovery->Start();
	} catch (...) {
156
		delete discovery;
157
		UpnpClientGlobalFinish();
158
		throw;
159 160 161 162 163 164
	}
}

void
UpnpDatabase::Close()
{
165
	delete discovery;
166
	UpnpClientGlobalFinish();
167 168 169
}

void
170
UpnpDatabase::ReturnSong(const LightSong *_song) const
171
{
172
	assert(_song != nullptr);
173

174 175
	UpnpSong *song = (UpnpSong *)const_cast<LightSong *>(_song);
	delete song;
176 177 178 179
}

// Get song info by path. We can receive either the id path, or the titles
// one
180
const LightSong *
181
UpnpDatabase::GetSong(const char *uri) const
182 183
{
	auto vpath = stringToTokens(uri, "/", true);
184 185 186
	if (vpath.size() < 2)
		throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
				    "No such song");
187

188
	auto server = discovery->GetServer(vpath.front().c_str());
189

190 191
	vpath.pop_front();

192
	UPnPDirObject dirent;
193
	if (vpath.front() != rootid) {
194
		dirent = Namei(server, vpath);
195
	} else {
196
		dirent = ReadNode(server, vpath.back().c_str());
197 198
	}

199
	return new UpnpSong(std::move(dirent), uri);
200 201 202 203 204 205 206 207
}

/**
 * Double-quote a string, adding internal backslash escaping.
 */
static void
dquote(std::string &out, const char *in)
{
208
	out.push_back('"');
209 210 211 212 213

	for (; *in != 0; ++in) {
		switch(*in) {
		case '\\':
		case '"':
214
			out.push_back('\\');
215 216
			break;
		}
217 218

		out.push_back(*in);
219 220
	}

221
	out.push_back('"');
222 223 224 225
}

// Run an UPnP search, according to MPD parameters. Return results as
// UPnP items
226
UPnPDirContent
227
UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
228
			  const char *objid,
229
			  const DatabaseSelection &selection) const
230 231 232
{
	const SongFilter *filter = selection.filter;
	if (selection.filter == nullptr)
233
		return UPnPDirContent();
234

235
	const auto searchcaps = server.getSearchCapabilities(handle);
236
	if (searchcaps.empty())
237
		return UPnPDirContent();
238 239 240 241 242 243 244 245 246

	std::string cond;
	for (const auto &item : filter->GetItems()) {
		switch (auto tag = item.GetTag()) {
		case LOCATE_TAG_ANY_TYPE:
			{
				if (!cond.empty()) {
					cond += " and ";
				}
247
				cond += '(';
248 249 250 251 252 253 254 255 256 257 258 259
				bool first(true);
				for (const auto& cap : searchcaps) {
					if (first)
						first = false;
					else
						cond += " or ";
					cond += cap;
					if (item.GetFoldCase()) {
						cond += " contains ";
					} else {
						cond += " = ";
					}
260
					dquote(cond, item.GetValue());
261
				}
262
				cond += ')';
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
			}
			break;

		default:
			/* Unhandled conditions like
			   LOCATE_TAG_BASE_TYPE or
			   LOCATE_TAG_FILE_TYPE won't have a
			   corresponding upnp prop, so they will be
			   skipped */
			if (tag == TAG_ALBUM_ARTIST)
				tag = TAG_ARTIST;

			// TODO: support LOCATE_TAG_ANY_TYPE etc.
			const char *name = tag_table_lookup(upnp_tags,
							    TagType(tag));
			if (name == nullptr)
				continue;

			if (!cond.empty()) {
				cond += " and ";
			}
			cond += name;

			/* FoldCase doubles up as contains/equal
			   switch. UpNP search is supposed to be
			   case-insensitive, but at least some servers
			   have the same convention as mpd (e.g.:
			   minidlna) */
			if (item.GetFoldCase()) {
				cond += " contains ";
			} else {
				cond += " = ";
			}
296
			dquote(cond, item.GetValue());
297 298 299
		}
	}

300
	return server.search(handle, objid, cond.c_str());
301 302
}

303
static void
304
visitSong(const UPnPDirObject &meta, const char *path,
305
	  const DatabaseSelection &selection,
306
	  VisitSong visit_song)
307 308
{
	if (!visit_song)
309
		return;
310

311 312
	LightSong song;
	song.directory = nullptr;
313
	song.uri = path;
314 315 316
	song.real_uri = meta.url.c_str();
	song.tag = &meta.tag;
	song.mtime = 0;
317
	song.start_time = song.end_time = SongTime::zero();
318

319 320
	if (selection.Match(song))
		visit_song(song);
321 322 323 324 325 326 327
}

/**
 * Build synthetic path based on object id for search results. The use
 * of "rootid" is arbitrary, any name that is not likely to be a top
 * directory name would fit.
 */
328
static std::string
329 330 331 332 333 334
songPath(const std::string &servername,
	 const std::string &objid)
{
	return servername + "/" + rootid + "/" + objid;
}

335
void
336
UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
337 338
			  const char *objid,
			  const DatabaseSelection &selection,
339
			  VisitSong visit_song) const
340 341
{
	if (!visit_song)
342
		return;
343

344
	for (auto &dirent : SearchSongs(server, objid, selection).objects) {
345 346 347 348
		if (dirent.type != UPnPDirObject::Type::ITEM ||
		    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
			continue;

349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
		// We get song ids as the result of the UPnP search. But our
		// client expects paths (e.g. we get 1$4$3788 from minidlna,
		// but we need to translate to /Music/All_Music/Satisfaction).
		// We can do this in two ways:
		//  - Rebuild a normal path using BuildPath() which is a kind of pwd
		//  - Build a bogus path based on the song id.
		// The first method is nice because the returned paths are pretty, but
		// it has two big problems:
		//  - The song paths are ambiguous: e.g. minidlna returns all search
		//    results as being from the "All Music" directory, which can
		//    contain several songs with the same title (but different objids)
		//  - The performance of BuildPath() is atrocious on very big
		//    directories, even causing timeouts in clients. And of
		//    course, 'All Music' is very big.
		// So we return synthetic and ugly paths based on the object id,
		// which we later have to detect.
365
		const std::string path = songPath(server.getFriendlyName(),
366
						  dirent.id);
367 368
		visitSong(std::move(dirent), path.c_str(),
			  selection, visit_song);
369 370 371
	}
}

372
UPnPDirObject
373
UpnpDatabase::ReadNode(const ContentDirectoryService &server,
374
		       const char *objid) const
375
{
376
	auto dirbuf = server.getMetadata(handle, objid);
377 378
	if (dirbuf.objects.size() != 1)
		throw std::runtime_error("Bad resource");
379

380
	return std::move(dirbuf.objects.front());
381 382
}

383
std::string
384
UpnpDatabase::BuildPath(const ContentDirectoryService &server,
385
			const UPnPDirObject& idirent) const
386
{
387
	const char *pid = idirent.id.c_str();
388
	std::string path;
389
	while (strcmp(pid, rootid) != 0) {
390
		auto dirent = ReadNode(server, pid);
391
		pid = dirent.parent_id.c_str();
392 393 394 395 396 397

		if (path.empty())
			path = dirent.name;
		else
			path = PathTraitsUTF8::Build(dirent.name.c_str(),
						     path.c_str());
398
	}
399

400
	return PathTraitsUTF8::Build(server.getFriendlyName(),
401
				     path.c_str());
402 403 404
}

// Take server and internal title pathname and return objid and metadata.
405
UPnPDirObject
406
UpnpDatabase::Namei(const ContentDirectoryService &server,
407
		    const std::list<std::string> &vpath) const
408
{
409
	if (vpath.empty())
410
		// looking for root info
411
		return ReadNode(server, rootid);
412

413 414
	std::string objid(rootid);

415
	// Walk the path elements, read each directory and try to find the next one
416
	for (auto i = vpath.begin(), last = std::prev(vpath.end());; ++i) {
417
		auto dirbuf = server.readDir(handle, objid.c_str());
418 419

		// Look for the name in the sub-container list
420
		UPnPDirObject *child = dirbuf.FindObject(i->c_str());
421 422 423
		if (child == nullptr)
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "No such object");
424

425 426
		if (i == last)
			return std::move(*child);
427

428 429 430
		if (child->type != UPnPDirObject::Type::CONTAINER)
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "Not a container");
431

432
		objid = std::move(child->id);
433 434 435
	}
}

436
static void
437 438
VisitItem(const UPnPDirObject &object, const char *uri,
	  const DatabaseSelection &selection,
439
	  VisitSong visit_song, VisitPlaylist visit_playlist)
440 441 442 443 444
{
	assert(object.type == UPnPDirObject::Type::ITEM);

	switch (object.item_class) {
	case UPnPDirObject::ItemClass::MUSIC:
445 446
		visitSong(object, uri, selection, visit_song);
		break;
447 448 449 450 451 452 453 454 455 456 457

	case UPnPDirObject::ItemClass::PLAYLIST:
		if (visit_playlist) {
			/* Note: I've yet to see a
			   playlist item (playlists
			   seem to be usually handled
			   as containers, so I'll
			   decide what to do when I
			   see one... */
		}

458
		break;
459 460

	case UPnPDirObject::ItemClass::UNKNOWN:
461
		break;
462 463 464
	}
}

465
static void
466 467 468 469
VisitObject(const UPnPDirObject &object, const char *uri,
	    const DatabaseSelection &selection,
	    VisitDirectory visit_directory,
	    VisitSong visit_song,
470
	    VisitPlaylist visit_playlist)
471 472 473 474 475 476 477
{
	switch (object.type) {
	case UPnPDirObject::Type::UNKNOWN:
		assert(false);
		gcc_unreachable();

	case UPnPDirObject::Type::CONTAINER:
478 479 480
		if (visit_directory)
			visit_directory(LightDirectory(uri, 0));
		break;
481 482

	case UPnPDirObject::Type::ITEM:
483 484 485
		VisitItem(object, uri, selection,
			  visit_song, visit_playlist);
		break;
486 487 488
	}
}

489 490
// vpath is a parsed and writeable version of selection.uri. There is
// really just one path parameter.
491
void
492
UpnpDatabase::VisitServer(const ContentDirectoryService &server,
493
			  const std::list<std::string> &vpath,
494 495 496
			  const DatabaseSelection &selection,
			  VisitDirectory visit_directory,
			  VisitSong visit_song,
497
			  VisitPlaylist visit_playlist) const
498 499 500 501 502 503 504 505 506
{
	/* If the path begins with rootid, we know that this is a
	   song, not a directory (because that's how we set things
	   up). Just visit it. Note that the choice of rootid is
	   arbitrary, any value not likely to be the name of a top
	   directory would be ok. */
	/* !Note: this *can't* be handled by Namei further down,
	   because the path is not valid for traversal. Besides, it's
	   just faster to access the target node directly */
507
	if (!vpath.empty() && vpath.front() == rootid) {
508 509
		switch (vpath.size()) {
		case 1:
510
			return;
511 512 513 514 515

		case 2:
			break;

		default:
516 517
			throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
					    "Not found");
518 519
		}

520
		if (visit_song) {
521
			auto dirent = ReadNode(server, vpath.back().c_str());
522

523
			if (dirent.type != UPnPDirObject::Type::ITEM ||
524 525 526
			    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
				throw DatabaseError(DatabaseErrorCode::NOT_FOUND,
						    "Not found");
527

528
			std::string path = songPath(server.getFriendlyName(),
529
						    dirent.id);
530 531
			visitSong(std::move(dirent), path.c_str(),
				  selection, visit_song);
532
		}
533 534

		return;
535 536 537
	}

	// Translate the target path into an object id and the associated metadata.
538
	const auto tdirent = Namei(server, vpath);
539 540 541 542 543

	/* If recursive is set, this is a search... No use sending it
	   if the filter is empty. In this case, we implement limited
	   recursion (1-deep) here, which will handle the "add dir"
	   case. */
544 545 546 547
	if (selection.recursive && selection.filter) {
		SearchSongs(server, tdirent.id.c_str(), selection, visit_song);
		return;
	}
548

Max Kellermann's avatar
Max Kellermann committed
549 550 551 552
	const char *const base_uri = selection.uri.empty()
		? server.getFriendlyName()
		: selection.uri.c_str();

553
	if (tdirent.type == UPnPDirObject::Type::ITEM) {
554 555 556 557
		VisitItem(tdirent, base_uri,
			  selection,
			  visit_song, visit_playlist);
		return;
558 559 560 561 562
	}

	/* Target was a a container. Visit it. We could read slices
	   and loop here, but it's not useful as mpd will only return
	   data to the client when we're done anyway. */
563
	for (const auto &dirent : server.readDir(handle, tdirent.id.c_str()).objects) {
564 565
		const std::string uri = PathTraitsUTF8::Build(base_uri,
							      dirent.name.c_str());
566 567 568 569
		VisitObject(dirent, uri.c_str(),
			    selection,
			    visit_directory,
			    visit_song, visit_playlist);
570 571 572 573
	}
}

// Deal with the possibly multiple servers, call VisitServer if needed.
574
void
575 576 577
UpnpDatabase::Visit(const DatabaseSelection &selection,
		    VisitDirectory visit_directory,
		    VisitSong visit_song,
578
		    VisitPlaylist visit_playlist) const
579 580 581
{
	auto vpath = stringToTokens(selection.uri, "/", true);
	if (vpath.empty()) {
582
		for (const auto &server : discovery->GetDirectories()) {
583
			if (visit_directory) {
584
				const LightDirectory d(server.getFriendlyName(), 0);
585
				visit_directory(d);
586
			}
587

588 589 590 591
			if (selection.recursive)
				VisitServer(server, vpath, selection,
					    visit_directory, visit_song,
					    visit_playlist);
592
		}
593

594
		return;
595 596 597
	}

	// We do have a path: the first element selects the server
598
	std::string servername(std::move(vpath.front()));
599
	vpath.pop_front();
600

601
	auto server = discovery->GetServer(servername.c_str());
602 603
	VisitServer(server, vpath, selection,
		    visit_directory, visit_song, visit_playlist);
604 605
}

606
void
607
UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
608
			      TagType tag, gcc_unused tag_mask_t group_mask,
609
			      VisitTag visit_tag) const
610
{
611 612 613
	// TODO: use group_mask

	if (!visit_tag)
614
		return;
615 616

	std::set<std::string> values;
617
	for (auto& server : discovery->GetDirectories()) {
618
		const auto dirbuf = SearchSongs(server, rootid, selection);
619

620 621 622 623 624
		for (const auto &dirent : dirbuf.objects) {
			if (dirent.type != UPnPDirObject::Type::ITEM ||
			    dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
				continue;

625
			const char *value = dirent.tag.GetValue(tag);
626
			if (value != nullptr) {
627
#if CLANG_OR_GCC_VERSION(4,8)
628
				values.emplace(value);
629
#else
630
				values.insert(value);
631 632
#endif
			}
633 634 635
		}
	}

636 637 638
	for (const auto& value : values) {
		TagBuilder builder;
		builder.AddItem(tag, value.c_str());
639
		visit_tag(builder.Commit());
640
	}
641 642
}

643 644
DatabaseStats
UpnpDatabase::GetStats(const DatabaseSelection &) const
645 646 647
{
	/* Note: this gets called before the daemonizing so we can't
	   reallyopen this would be a problem if we had real stats */
648
	DatabaseStats stats;
649
	stats.Clear();
650
	return stats;
651 652 653 654
}

const DatabasePlugin upnp_db_plugin = {
	"upnp",
655
	0,
656 657
	UpnpDatabase::Create,
};