UpnpDatabasePlugin.cxx 16.9 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2017 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/Builder.hxx"
#include "tag/Table.hxx"
38
#include "tag/Mask.hxx"
39
#include "fs/Traits.hxx"
40 41 42 43 44 45 46 47 48 49 50 51
#include "Log.hxx"
#include "SongFilter.hxx"

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

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

static const char *const rootid = "0";

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

	Tag tag2;

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

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

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

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

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

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

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

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

97 98
	std::chrono::system_clock::time_point GetUpdateStamp() const override {
		return std::chrono::system_clock::time_point::min();
99
	}
100 101

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

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

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

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

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

	/**
	 * 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).
	 */
136 137
	std::string BuildPath(const ContentDirectoryService &server,
			      const UPnPDirObject& dirent) const;
138 139 140
};

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

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

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

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

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

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

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

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

191 192
	vpath.pop_front();

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

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

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

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

		out.push_back(*in);
220 221
	}

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

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

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

	std::string cond;
	for (const auto &item : filter->GetItems()) {
		switch (auto tag = item.GetTag()) {
		case LOCATE_TAG_ANY_TYPE:
			{
				if (!cond.empty()) {
					cond += " and ";
				}
248
				cond += '(';
249 250 251 252 253 254 255 256 257 258 259 260
				bool first(true);
				for (const auto& cap : searchcaps) {
					if (first)
						first = false;
					else
						cond += " or ";
					cond += cap;
					if (item.GetFoldCase()) {
						cond += " contains ";
					} else {
						cond += " = ";
					}
261
					dquote(cond, item.GetValue());
262
				}
263
				cond += ')';
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 296
			}
			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 += " = ";
			}
297
			dquote(cond, item.GetValue());
298 299 300
		}
	}

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

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

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

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

/**
 * 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.
 */
329
static std::string
330 331 332 333 334 335
songPath(const std::string &servername,
	 const std::string &objid)
{
	return servername + "/" + rootid + "/" + objid;
}

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

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

350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
		// 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.
366
		const std::string path = songPath(server.getFriendlyName(),
367
						  dirent.id);
368 369
		visitSong(std::move(dirent), path.c_str(),
			  selection, visit_song);
370 371 372
	}
}

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

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

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

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

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

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

414 415
	std::string objid(rootid);

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

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

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

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

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

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

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

	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... */
		}

459
		break;
460 461

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

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

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

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

490 491
// vpath is a parsed and writeable version of selection.uri. There is
// really just one path parameter.
492
void
493
UpnpDatabase::VisitServer(const ContentDirectoryService &server,
494
			  const std::list<std::string> &vpath,
495 496 497
			  const DatabaseSelection &selection,
			  VisitDirectory visit_directory,
			  VisitSong visit_song,
498
			  VisitPlaylist visit_playlist) const
499 500 501 502 503 504 505 506 507
{
	/* 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 */
508
	if (!vpath.empty() && vpath.front() == rootid) {
509 510
		switch (vpath.size()) {
		case 1:
511
			return;
512 513 514 515 516

		case 2:
			break;

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

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

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

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

		return;
536 537 538
	}

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

	/* 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. */
545 546 547 548
	if (selection.recursive && selection.filter) {
		SearchSongs(server, tdirent.id.c_str(), selection, visit_song);
		return;
	}
549

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

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

	/* 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. */
564
	for (const auto &dirent : server.readDir(handle, tdirent.id.c_str()).objects) {
565 566
		const std::string uri = PathTraitsUTF8::Build(base_uri,
							      dirent.name.c_str());
567 568 569 570
		VisitObject(dirent, uri.c_str(),
			    selection,
			    visit_directory,
			    visit_song, visit_playlist);
571 572 573 574
	}
}

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

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

595
		return;
596 597 598
	}

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

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

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

	if (!visit_tag)
615
		return;
616 617

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

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

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

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

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

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